using UnityEditor; using UnityEngine; using System; using System.Threading; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Text; using Perforce.P4; using log4net; namespace P4Connect { /// /// Simple asset status class /// Stores the file state (checked out, modified, etc...) and revision state /// public class AssetStatus { public enum LastUpdateType { OnServer, OffServer, Default } public DateTime TimeStamp; public FileState LocalState; public FileState OtherState; public DepotState DepotState; public RevisionState RevisionState; public ResolvedState ResolvedState; public StorageType StorageType; public LockState LockState; public LastUpdateType UpdateType; private static readonly GUILayoutOption _nameWidth = GUILayout.Width(100); private static GUILayoutOption _valueWidth = GUILayout.Width(120); // Use this to create an AssetStatus for a local file (perforce would return null from the fstat call) public AssetStatus() { TimeStamp = default(DateTime); LocalState = FileState.None; OtherState = FileState.None; DepotState = DepotState.None; RevisionState = P4Connect.RevisionState.None; ResolvedState = P4Connect.ResolvedState.None; StorageType = StorageType.Text; LockState = LockState.None; UpdateType = LastUpdateType.Default; } public override string ToString() { StringBuilder sb = new StringBuilder(); if (TimeStamp == default(DateTime)) { sb.AppendFormat("Dummy AssetStatus"); } else { sb.AppendFormat("TimeStamp: {0} ", TimeStamp); sb.AppendFormat("UpdateType: {0} ", UpdateType); sb.AppendFormat("LocalState: {0} ", LocalState); sb.AppendFormat("OtherState: {0} ", OtherState); sb.AppendFormat("DepotState: {0} ", DepotState); sb.AppendFormat("Revision: {0} ", RevisionState); sb.AppendFormat("Resolved: {0} ", ResolvedState); sb.AppendFormat("Storage: {0} ", StorageType); sb.AppendFormat("Lockstate: {0} ", LockState); } return (sb.ToString()); } public bool IsOnServer() { return (LocalState != FileState.None && LocalState != FileState.MarkedForAdd); } public bool IsAddable() { return (LocalState == FileState.None || (LocalState == FileState.InDepot && DepotState == DepotState.Deleted)); } public bool IsEditable() { return (LocalState == FileState.InDepot && (DepotState != DepotState.Deleted)); } public bool IsLocked() { return (LockState == LockState.OurLock || LockState == LockState.TheirLock); } public bool IsLockable() { return (LockState != LockState.TheirLock && IsOpened() && !IsLocked()); } public bool IsUnlockable() { return (LockState == LockState.OurLock && IsOpened()); } public bool IsOpened() { return !(LocalState == FileState.None || LocalState == FileState.InDepot); } public bool IsMarked() { return (LocalState == FileState.MarkedForAdd || LocalState == FileState.MarkedForAddMove || LocalState == FileState.MarkedForDelete || LocalState == FileState.MarkedForDeleteMove || LocalState == FileState.MarkedForEdit); } public bool IsInitialized() { return (this.TimeStamp != default(DateTime)); } /// /// Display Status in Editor Window /// public void OnGUI() { GUILayout.BeginVertical("box"); GUILayout.BeginHorizontal(); GUILayout.Label("TimeStamp: ", _nameWidth); GUILayout.Label(TimeStamp.ToString("u"), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("UpdateType: ", _nameWidth); GUILayout.Label(UpdateType.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("LocalState: ", _nameWidth); GUILayout.Label(LocalState.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("OtherState: ", _nameWidth); GUILayout.Label(OtherState.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("DepotState: ", _nameWidth); GUILayout.Label(DepotState.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("RevisionState: ", _nameWidth); GUILayout.Label(RevisionState.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("ResolvedState: ", _nameWidth); GUILayout.Label(ResolvedState.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("StorageType: ", _nameWidth); GUILayout.Label(StorageType.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("LockState: ", _nameWidth); GUILayout.Label(LockState.ToString(), _valueWidth); GUILayout.EndHorizontal(); GUILayout.EndVertical(); } } /// /// Stores and periodically updates the Perforce status of all the assets /// /// /// /// This class is used to store the cached results for each "node" we keep track of in this project /// public class AssetEntry { public AssetStatus Status; public FileMetaData Meta; public DateTime Update; public ReconcileEntry Reconcile; public List OpenedList; public AssetEntry() { Status = new AssetStatus(); Meta = new FileMetaData(); Status.TimeStamp = Update = default(DateTime); OpenedList = new List(); } public bool Dirty { get { return (Update == default(DateTime)); } set { if (value == true) { Status.TimeStamp = Update = default(DateTime); } } } public void AddOpenedFile(File entry) { OpenedList.Add(entry); } } public class AssetStatusCache { private static readonly ILog log = LogManager.GetLogger(typeof(AssetStatusCache)); private static object _lockUpdated = new object(); // lock object for thread response. private static bool _queryDone = false; private static string[] _queryContent; private static float _pollRate = 10; // every 10 seconds private static DateTime _lastRefresh; /// /// This delegate is triggered when the status of an asset changes /// public delegate void OnAssetStatusChangedDelegate(PerforceConnection aConnection); public static event OnAssetStatusChangedDelegate OnAssetStatusChanged { add { if (_OnAssetStatusChanged == null) { // We had no reason to get updated before, so do it now Engine.OnOperationPerformed += OnEngineOperationPerformed; } _OnAssetStatusChanged += value; } remove { _OnAssetStatusChanged -= value; if (_OnAssetStatusChanged == null) { // We no longer have any reason to be updated Engine.OnOperationPerformed -= OnEngineOperationPerformed; } } } // The internal event that is actually registered with static event OnAssetStatusChangedDelegate _OnAssetStatusChanged; // The caches static Dictionary _Entries; // The background fstat thread private static MetaDataQueue _dataQueue; public static void Initialize() { _Entries = new Dictionary(); PendingDirty = true; _dataQueue = new MetaDataQueue(); _queryDone = false; _lastRefresh = DateTime.Now; EditorApplication.update += OnUpdate; } /// /// Run periodically to see if Icons need to be updated in the project windows. /// CheckForUpdate() gets set to true when the BG thread is done with some fstat fetches. /// static void OnUpdate() { if (Config.ValidConfiguration && Config.DisplayStatusIcons && Config.IsEditing()) { if (CheckForUpdate()) { //log.Debug("AssetStatusCache OnUpdate"); // Update ChangeManager with new fstats ChangeManager.OnUpdatedFiles(LastUpdateStrings()); // Refresh the Project Window Icons.UpdateDisplay(); ClearUpdate(); } } } /// /// Used by the Background Thread to signal completion of a file status retrieval /// public static void SignalUpdate(string[] files) { lock (_lockUpdated) { _queryDone = true; _queryContent = files; } } /// /// Returns true if a file status update is pending /// Also resets the query state at the same time. /// /// true public static bool CheckForUpdate() { bool rv = false; lock (_lockUpdated) { rv = _queryDone; //if (rv) log.Debug("Signal!"); } return rv; } public static string[] LastUpdateStrings() { return _queryContent; } public static void ClearUpdate() { lock (_lockUpdated) { _queryDone = false; } } /// /// Generic method that fetches the cached AssetEntry which includes /// AssetStatus (checked out, out of date, etc...) /// FileMetaData (server "fstat" result) /// public static AssetEntry GetAssetEntry(string aAssetPath) { AssetEntry entry; string path = Utils.GetEffectivePath(aAssetPath); if (!_Entries.TryGetValue(path, out entry)) { //log.Debug("Creating DEFAULT: " + path); entry = new AssetEntry(); _Entries[path] = entry; } return entry; } /// /// Get a cached asset status, don't go to the server for it. /// /// Asset Path /// AssetStatus public static AssetStatus GetCachedAssetStatus(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.Status; } /// /// Get a cached asset metadata, don't go to the server for it. /// /// Asset Path /// FileMetaData public static FileMetaData GetCachedMetaData(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.Meta; } /// /// Get a cached asset metadata, don't go to the server for it. /// /// FileSpec /// FileMetaData public static FileMetaData GetCachedMetaData(FileSpec spec) { string path = Utils.LocalPathToAssetPath(spec.LocalPath.Path); AssetEntry entry = GetAssetEntry(path); return entry.Meta; } public static DateTime GetCacheUpdated(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.Update; } /// /// Return cached asset status. /// If it has never come from the server (been initialized) put in a background /// request to update it. /// /// Asset path /// AssetStatus public static AssetStatus GetCachedAssetStatusAndUpdateDefaults(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); if (!entry.Status.IsInitialized()) { string[] paths = {aAssetPath}; GetFstatsFromServer(paths); } return entry.Status; } /// /// Return cached asset MetaData. /// If it has never come from the server (been initialized) put in a background /// request to update it. /// /// Asset path /// FileMetaData public static FileMetaData GetCachedMetaDataAndUpdateDefaults(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); if (!entry.Status.IsInitialized()) { string[] paths = { aAssetPath }; GetFstatsFromServer(paths); } return entry.Meta; } // A flag for pending changes to check if something has changedIgnore file name public static bool PendingDirty { get; set; } /// /// Given the meta data, update AssetStatus which drives the P4Connect icon presentation /// public static void AssetStatusFromMetaData(AssetStatus status, FileMetaData aMeta) { // Debug.Log("StoreAssetStatus: " + Logger.FileMetaDataToString(aMeta)); if (aMeta == null) return; // Fill out the struct switch (aMeta.Type.BaseType) { case BaseFileType.Text: status.StorageType = StorageType.Text; break; case BaseFileType.Binary: status.StorageType = StorageType.Binary; break; default: status.StorageType = StorageType.Other; break; } status.LocalState = Engine.ParseFileAction(aMeta.Action); if (status.LocalState == FileState.None) { // Check to see if Perforce knows this file so we can override the localState to InDepot. if (aMeta.IsMapped || aMeta.HeadAction != FileAction.None) status.LocalState = FileState.InDepot; } if (aMeta.OtherActions != null) { status.OtherState = Engine.ParseFileAction(aMeta.OtherActions[0]); } if (aMeta.HaveRev >= aMeta.HeadRev) status.RevisionState = RevisionState.HasLatest; else status.RevisionState = RevisionState.OutOfDate; if (aMeta.Unresolved) status.ResolvedState = ResolvedState.NeedsResolve; else status.ResolvedState = ResolvedState.None; if (aMeta.HeadAction == FileAction.MoveDelete || aMeta.HeadAction == FileAction.Delete) status.DepotState = DepotState.Deleted; else status.DepotState = DepotState.None; status.TimeStamp = DateTime.Now; status.LockState = Engine.GetLockState(aMeta); status.UpdateType = AssetStatus.LastUpdateType.OnServer; // Debug.Log("AssetStatus: " + status.ToString()); } /// /// Given a Reconcile entry, update AssetStatus which drives the P4Connect icon presentation /// public static void AssetStatusFromReconcileEntry(AssetStatus status, ReconcileEntry entry) { // Debug.Log("AssetStatusFromReconcileEntry: " + Logger.ReconcileEntryToString(entry)); if (entry == null) return; // Fill out the struct switch (entry.Type.BaseType) { case BaseFileType.Text: status.StorageType = StorageType.Text; break; case BaseFileType.Binary: status.StorageType = StorageType.Binary; break; default: status.StorageType = StorageType.Other; break; } status.LocalState = Engine.ParseFileAction(entry.Action); // Otherstate is unknown when generated by ReconcileEntry //status.OtherState = FileState.None; if (entry.WorkRev.Equals(entry.Version)) { status.RevisionState = RevisionState.HasLatest; } else { status.RevisionState = (entry.WorkRev.Rev == 0) ? RevisionState.None : RevisionState.OutOfDate; } if (entry.Action == FileAction.Unresolved) status.ResolvedState = ResolvedState.NeedsResolve; else status.ResolvedState = ResolvedState.None; if (entry.Action == FileAction.MoveDelete || entry.Action == FileAction.Delete) status.DepotState = DepotState.Deleted; else status.DepotState = DepotState.None; status.TimeStamp = DateTime.Now; status.UpdateType = AssetStatus.LastUpdateType.OnServer; // LockState is unknown when generated by ReconcileEntry //status.LockState = Engine.GetLockState(aMeta); //Debug.Log("AssetStatus: " + status.ToString()); } /// /// Update an asset status from a File entry (result of p4 opened) /// /// /// /// public static void UpdateAssetStatusFromFile(AssetStatus status, File entry) { //Debug.Log("UpdateAssetStatusFromFile: " + entry.ToStringNullSafe()); if (entry == null) return; // Fill out the struct switch (entry.Type.BaseType) { case BaseFileType.Text: status.StorageType = StorageType.Text; break; case BaseFileType.Binary: status.StorageType = StorageType.Binary; break; default: status.StorageType = StorageType.Other; break; } // see if this entry is from our local workspace if (entry.User == Config.Username && entry.Client == Config.Workspace) { status.LocalState = Engine.ParseFileAction(entry.Action); if (entry.HaveRev.Equals(entry.Version)) { status.RevisionState = RevisionState.HasLatest; } else { status.RevisionState = (entry.HaveRev.Rev == 0) ? RevisionState.None : RevisionState.OutOfDate; } status.ResolvedState = (entry.Action == FileAction.Unresolved) ? ResolvedState.NeedsResolve : ResolvedState.None; if (entry.Action == FileAction.MoveDelete || entry.Action == FileAction.Delete) status.DepotState = DepotState.Deleted; else status.DepotState = DepotState.None; status.LockState = entry.Locked ? LockState.OurLock : LockState.None; } else // Must be someone using my files in a remote location { status.OtherState = Engine.ParseFileAction(entry.Action); if (status.LockState == LockState.None && entry.Locked) status.LockState = LockState.TheirLock; } status.TimeStamp = DateTime.Now; status.UpdateType = AssetStatus.LastUpdateType.OnServer; if (entry.Locked) status.LockState = LockState.OurLock; //Debug.Log("AssetStatus: " + status.ToString()); return; } /// /// We have new Metadata so Update the cache /// /// IEnumerable MetaDataList public static void UpdateCacheWithMetadata(IEnumerable fmdlist) { foreach (var fmd in fmdlist) { var entry = GetAssetEntry(fmd.ToAssetPath()); if (entry != null) { entry.Meta = fmd; // Since the Metadata changed, re-create the Asset Status Also AssetStatusFromMetaData(entry.Status, fmd); } } } /// /// This functions gets called when P4Connect does *something* to files /// In this case we update the files status /// static void OnEngineOperationPerformed(PerforceConnection aConnection, IEnumerable aFiles, AssetOperation op) { // ToDo: Look at filtering by operation. Some of these Fstats may have already been updated GetFstatsFromServer(aFiles); // retrieve new fstats in the background } /// /// Given a collection of folders and Asset paths, update file Metadata information from the server /// Queues an ASYNCHRONOUS request for the asset paths to a background thread /// /// collection of Asset paths, and folders public static void GetFstatsFromServer(IEnumerable paths) { if (paths.Any()) { string[] paths1 = paths.AddDirectoryWildcards().ToArray(); //log.Debug("GetFstatsFromServer: " + Logger.StringArrayToString(paths1)); _dataQueue.QueueQuery(paths1); } } /// /// Checks current cached asset status, and if not initialized, retrieves it from the server /// Updates run ASYNCRONOUSLY /// /// assetpaths to retrieve from server if still dummies public static void UpdateDefaultsFromServer(IEnumerable paths) { var updatePaths = new HashSet(); foreach (string t in paths) { if (! Utils.IsDirectory(t)) // Skip subdirectories { var status = GetCachedAssetStatus(t); if (!status.IsInitialized()) { updatePaths.Add(t); } } } if (updatePaths.Count > 0) { GetFstatsFromServer(updatePaths); } } /// /// Given a folder, invalidate all cache data for members /// /// Path to dirty folder public static void MarkFolderAsDirty(string path) { path = Utils.RemoveDirectoryWildcards(path); log.Debug("dirtypath: " + path); IEnumerable fullKeys = _Entries.Keys.Where(currentKey => currentKey.StartsWith(path)); foreach (string key in fullKeys) { _Entries[key].Dirty = true; log.Debug("dirt: " + key); } } /// /// Given an asset path, mark this asset as Dirty /// So it will be re-fstated later. /// /// public static void MarkAsDirty(string assetPath) { //log.Debug("DIRTY: " + assetPath); var entry = GetAssetEntry(assetPath); entry.Update = default(DateTime); entry.Status.TimeStamp = entry.Update; } /// /// Mark the contents of a List of FileAndMetas as dirty /// /// FileAndMetas Collection static public void MarkAsDirty(IEnumerable fam) { if (fam == null) return; foreach (var f in fam) { if (f.File != null) { MarkAsDirty(f.File.ToAssetPath()); } if (f.Meta != null) { MarkAsDirty(f.Meta.ToAssetPath()); } } } /// /// Using the Perforce connection, get a matching AssetStatus for each Assetpath /// /// Perforce Connection /// A List of asset paths to retrieve /// A list of statuses, one per aFile //public static void GetAssetStatusesFromPaths(PerforceConnection aConnection, List aFiles, // List aOutStatuses) //{ // GetAssetStatuses(aConnection, GetEffectiveFileSpecs(aFiles).ToList(), aOutStatuses); //} /// /// Using Perforce connection, get a matching AssetStatus for each Assetpath /// /// Perforce Connection /// A List of asset paths to retrieve /// A List of AssetStatus, one for each entry //static public List GetAssetStatusesFromPaths(PerforceConnection aConnection, // List aFiles) //{ // List aOutStatuses = new List(); // GetAssetStatusesFromPaths(aConnection, aFiles, aOutStatuses); // return aOutStatuses; //} /// /// Using the Perforce connection, get a matching AssetStatus for each FileSpec /// If the metadata for a file is not in Perforce, we provide a "local" AssetStatus instead. /// /// a valid Perforce Connection /// A list of FileSpecs to be looked up /// an AssetStatus is added to this list for each entry //static void GetAssetStatuses(PerforceConnection aConnection, List aFiles, // List aOutStatuses) //{ // IEnumerable assetNames = GetAssetPathsFromFileSpecs(aFiles); // string[] files = assetNames.ToArray(); // Debug.Log("retrieving: " + Logger.StringArrayToString(files)); // List notInitialized = new List(); // // Collect results here for return later // AssetStatus[] stats = new AssetStatus[files.Count()]; // // Get The Asset Status for each file, if not initialized add to "not_initialized" list // for (int i = 0; i < files.Count(); i++) // { // stats[i] = GetCachedAssetStatus(files[i]); // if (!stats[i].IsInitialized()) // { // notInitialized.Add(files[i]); // } // } // // Now Ask for any uninitialized AssetStatus's from the server // if (notInitialized.Count > 0) // { // log.Debug("Going to server for: " + Logger.StringArrayToString(notInitialized.ToArray())); // GetFstatsFromServer(notInitialized); // } // // Finally update the "holes" in the statuses originally returned // for (int i = 0; i < files.Count(); i++) // { // if (!stats[i].IsInitialized()) // { // stats[i] = GetCachedAssetStatus(files[i]); // } // } // aOutStatuses.AddRange(stats); // log.Debug("assetstatuses: " + Logger.AssetStatusListToString(aOutStatuses)); //} /// /// Goes to the server, updates cache with latest about the files specified /// Frequently runs in a background thread. /// /// A valid Perforce connection /// AssetPath list to retrieve info about /// enumeration of asset paths which were updated public static IEnumerable GetAssetMetaData(PerforceConnection aConnection, IEnumerable aFiles) { //log.Debug("GetAssetMetaData: " + Logger.StringArrayToString(aFiles.ToArray())); var returnList = new HashSet(); if (aFiles.Any()) { HashSet original = new HashSet(aFiles); HashSet checkedList = null; DateTime startTimestamp = DateTime.Now; Options fsopts = new Options(); fsopts["-Op"] = null; // Return "real" clientpaths var mdlist = Engine.GetFileMetaData(aConnection, original.ToDepotSpecs().ToList(), fsopts); if (mdlist != null) { // with a good response, we store the retrieved meta data in the AssetCache //StorePerforceMetaData(mdlist); // This is done in Engine.GetFileMetaData() instead checkedList = new HashSet(mdlist.ToAssetPaths().EffectivePaths()); } // Remove wildcard entries from original list. // Create Local MetaData entries for files unknown by Perforce IEnumerable leftOvers = (checkedList != null) ? original.RemoveWildCards().Except(checkedList) : original; if (leftOvers.Any()) StoreLocalMetaData(leftOvers); if (Config.DisplayP4Timings) { double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds; aConnection.AppendTimingInfo("GetAssetMetaData " + deltaInnerTime + " ms"); } if (checkedList != null) { returnList.UnionWith(checkedList); } returnList.UnionWith(leftOvers); } return returnList; } /// /// We have fresh metadata from the server /// Lets store it in the Asset Cache /// /// List of Metadata to store public static void StorePerforceMetaData(IList mdlist) { if (mdlist == null) return; foreach (var md in mdlist) { if (md == null || md.LocalPath == null || md.LocalPath.Path == null || md.LocalPath.Path.Length == 0) { log.Debug("Missing LocalPath - MetaData: " + Logger.FileMetaDataToString(md)); continue; } // Create the AssetStatus and FileMetaData entries for each file known by the server string fname = Utils.GetEffectivePath(Utils.LocalPathToAssetPath(md.LocalPath.Path)); AssetEntry entry = new AssetEntry { Meta = md, Update = DateTime.Now }; AssetStatusFromMetaData(entry.Status, md); _Entries[fname] = entry; //log.Debug("AssetEntry: " + fname + " CREATED " + entry.Status.ToStringNullSafe()); } } /// /// The metadata from the server came back, and did not include these files /// So we need to make valid Cache entries as if they are Local files only /// /// Asset Paths of files to cache public static void StoreLocalMetaData(IEnumerable files) { if (files == null) return; // For "leftover" files not known by the server, we provide a default // AssetEntry with the update field set. foreach (string f in files) { AssetEntry entry = new AssetEntry(); entry.Meta = new FileMetaData(); entry.Status = new AssetStatus(); entry.Status.TimeStamp = entry.Update = DateTime.Now; entry.Status.UpdateType = AssetStatus.LastUpdateType.OffServer; // We verified that the server doesn't have a record of this. _Entries[f] = entry; //log.Debug("AssetEntry: " + f + " LOCAL " + entry.Status.ToStringNullSafe()); } } /// /// Returns a proper list of file spec given a list of paths, skipping folders, generating folder meta files instead /// /// Asset Path strings /// FileSpecs static IEnumerable GetEffectiveFileSpecs(IEnumerable aAssetPaths) { foreach (var path in aAssetPaths) { yield return FileSpec.LocalSpec(Utils.AssetPathToLocalPath(Utils.GetEffectivePath(path))); } yield break; } /// /// Translates a collection of asset paths (including folders) into a collection of LocalSpec FileSpecs /// /// Asset paths to become FileSpec /// FileSpecs public static IEnumerable GetFileSpecs(IEnumerable aAssetPaths) { foreach (var path in aAssetPaths) { yield return FileSpec.LocalSpec(Utils.AssetPathToLocalPath(path)); } yield break; } /// /// Return a collection of Asset paths given a list of FileSpecs /// (using the LocalPath of the FileSpec) /// /// FileSpecs /// strings public static IEnumerable GetAssetPathsFromFileSpecs(IEnumerable fs) { foreach (var f in fs) { yield return Utils.LocalPathToAssetPath(f.LocalPath.Path); } yield break; } /// /// Return a collection of Asset paths given a list of FileMetaData /// /// FileMetaData collection /// string collection public static IEnumerable GetAssetPathsFromFileMetaData(IEnumerable fmd_list) { foreach (var fm in fmd_list) { yield return Utils.LocalPathToAssetPath(fm.LocalPath.Path); } yield break; } #region InspectorGUI [CustomEditor(typeof(UnityEditor.DefaultAsset), true)] public class PerforceAssetPropertiesEditor : Editor { private AssetEntry _entry; private string _assetPath; public override void OnInspectorGUI() { //GUI.enabled = false; DrawDefaultInspector(); //GUI.enabled = true; //EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Perforce Properties", EditorStyles.boldLabel); EditorGUILayout.BeginVertical("box"); _assetPath = AssetDatabase.GetAssetPath(target.GetInstanceID()); EditorGUILayout.LabelField("Path: " + _assetPath); _entry = GetAssetEntry(_assetPath); if (_entry == null) { EditorGUILayout.LabelField("No Asset Entry Found"); } else { _entry.Status.OnGUI(); } var guid = AssetDatabase.AssetPathToGUID(_assetPath); EditorGUILayout.LabelField("GUID: " + guid); string[] dependents = AssetDatabase.GetDependencies(_assetPath, false); foreach (var dep in dependents) { EditorGUILayout.LabelField("-- " + dep); } EditorGUILayout.EndVertical(); } } #endregion #region MetaDataQueue /// /// Encapsulates a thread with an input queue of string arrays. /// Issues "p4 fstat" command for each entry /// class MetaDataQueue { private Thread _worker; readonly Queue _queue = new Queue(); readonly object _locker = new object(); readonly object _lockUpdated = new object(); private readonly EventWaitHandle _wh = new AutoResetEvent(false); // private bool queryDone = false; public MetaDataQueue() { _worker = new Thread(Work); _worker.Start(); } ~MetaDataQueue() { QueueQuery(null); _worker.Join(); _wh.Close(); } public void QueueQuery(string[] paths) { lock (_locker) _queue.Enqueue(paths); _wh.Set(); } /// /// Work - This is run by the background thread. /// It issues a command on the queue /// Then waits for an Event to proceed. /// private void Work() { while (true) { var argument = new HashSet(); lock (_locker) { while (_queue.Count > 0) { argument.UnionWith(_queue.Dequeue()); // Collect all pending requests into one list. } } if (argument.Count > 0) { string[] changedFiles = new string[0]; Engine.PerformConnectionOperation(con => { changedFiles = AssetStatusCache.GetAssetMetaData(con, argument).ToArray(); }, true); if (changedFiles.Length > 0) SignalUpdate(changedFiles); } else { _wh.WaitOne(); } } } } #endregion } }