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 { /// <summary> /// Simple asset status class /// Stores the file state (checked out, modified, etc...) and revision state /// </summary> 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)); } /// <summary> /// Display Status in Editor Window /// </summary> 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(); } } /// <summary> /// Stores and periodically updates the Perforce status of all the assets /// </summary> /// /// <summary> /// This class is used to store the cached results for each "node" we keep track of in this project /// </summary> public class AssetEntry { public AssetStatus Status; public FileMetaData Meta; public DateTime Update; public ReconcileEntry Reconcile; public List<File> OpenedList; public AssetEntry() { Status = new AssetStatus(); Meta = new FileMetaData(); Status.TimeStamp = Update = default(DateTime); OpenedList = new List<File>(); } 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; /// <summary> /// This delegate is triggered when the status of an asset changes /// </summary> 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<string, AssetEntry> _Entries; // The background fstat thread private static MetaDataQueue _dataQueue; public static void Initialize() { _Entries = new Dictionary<string, AssetEntry>(); PendingDirty = true; _dataQueue = new MetaDataQueue(); _queryDone = false; _lastRefresh = DateTime.Now; EditorApplication.update += OnUpdate; } /// <summary> /// 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. /// </summary> 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(); } } } /// <summary> /// Used by the Background Thread to signal completion of a file status retrieval /// </summary> public static void SignalUpdate(string[] files) { lock (_lockUpdated) { _queryDone = true; _queryContent = files; } } /// <summary> /// Returns true if a file status update is pending /// Also resets the query state at the same time. /// </summary> /// <returns>true</returns> 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; } } /// <summary> /// Generic method that fetches the cached AssetEntry which includes /// AssetStatus (checked out, out of date, etc...) /// FileMetaData (server "fstat" result) /// </summary> 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; } /// <summary> /// Get a cached asset status, don't go to the server for it. /// </summary> /// <param name="aAssetPath">Asset Path</param> /// <returns>AssetStatus</returns> public static AssetStatus GetCachedAssetStatus(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.Status; } /// <summary> /// Get a cached asset metadata, don't go to the server for it. /// </summary> /// <param name="aAssetPath">Asset Path</param> /// <returns>FileMetaData</returns> public static FileMetaData GetCachedMetaData(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.Meta; } /// <summary> /// Get a cached asset metadata, don't go to the server for it. /// </summary> /// <param name="spec">FileSpec</param> /// <returns>FileMetaData</returns> 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; } /// <summary> /// Return cached asset status. /// If it has never come from the server (been initialized) put in a background /// request to update it. /// </summary> /// <param name="aAssetPath">Asset path</param> /// <returns>AssetStatus</returns> public static AssetStatus GetCachedAssetStatusAndUpdateDefaults(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); if (!entry.Status.IsInitialized()) { string[] paths = {aAssetPath}; GetFstatsFromServer(paths); } return entry.Status; } /// <summary> /// Return cached asset MetaData. /// If it has never come from the server (been initialized) put in a background /// request to update it. /// </summary> /// <param name="aAssetPath">Asset path</param> /// <returns>FileMetaData</returns> 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; } /// <summary> /// Given the meta data, update AssetStatus which drives the P4Connect icon presentation /// </summary> 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()); } /// <summary> /// Given a Reconcile entry, update AssetStatus which drives the P4Connect icon presentation /// </summary> 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()); } /// <summary> /// Update an asset status from a File entry (result of p4 opened) /// </summary> /// <param name="status"></param> /// <param name="entry"></param> /// <returns></returns> 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; } /// <summary> /// We have new Metadata so Update the cache /// </summary> /// <param name="fmdlist">IEnumerable MetaDataList</param> public static void UpdateCacheWithMetadata(IEnumerable<FileMetaData> 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); } } } /// <summary> /// This functions gets called when P4Connect does *something* to files /// In this case we update the files status /// </summary> static void OnEngineOperationPerformed(PerforceConnection aConnection, IEnumerable <string> 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 } /// <summary> /// 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 /// </summary> /// <param name="paths">collection of Asset paths, and folders</param> public static void GetFstatsFromServer(IEnumerable<string> paths) { if (paths.Any()) { string[] paths1 = paths.AddDirectoryWildcards().ToArray(); //log.Debug("GetFstatsFromServer: " + Logger.StringArrayToString(paths1)); _dataQueue.QueueQuery(paths1); } } /// <summary> /// Checks current cached asset status, and if not initialized, retrieves it from the server /// Updates run ASYNCRONOUSLY /// </summary> /// <param name="paths">assetpaths to retrieve from server if still dummies</param> public static void UpdateDefaultsFromServer(IEnumerable<string> paths) { var updatePaths = new HashSet<string>(); 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); } } /// <summary> /// Given a folder, invalidate all cache data for members /// </summary> /// <param name="path">Path to dirty folder</param> public static void MarkFolderAsDirty(string path) { path = Utils.RemoveDirectoryWildcards(path); log.Debug("dirtypath: " + path); IEnumerable<string> fullKeys = _Entries.Keys.Where(currentKey => currentKey.StartsWith(path)); foreach (string key in fullKeys) { _Entries[key].Dirty = true; log.Debug("dirt: " + key); } } /// <summary> /// Given an asset path, mark this asset as Dirty /// So it will be re-fstated later. /// </summary> /// <param name="assetPath"></param> public static void MarkAsDirty(string assetPath) { //log.Debug("DIRTY: " + assetPath); var entry = GetAssetEntry(assetPath); entry.Update = default(DateTime); entry.Status.TimeStamp = entry.Update; } /// <summary> /// Mark the contents of a List of FileAndMetas as dirty /// </summary> /// <param name="fam">FileAndMetas Collection</param> static public void MarkAsDirty(IEnumerable<FileAndMeta> 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()); } } } /// <summary> /// Using the Perforce connection, get a matching AssetStatus for each Assetpath /// </summary> /// <param name="aConnection">Perforce Connection</param> /// <param name="aFiles">A List of asset paths to retrieve</param> /// <param name="aOutStatuses">A list of statuses, one per aFile</param> //public static void GetAssetStatusesFromPaths(PerforceConnection aConnection, List<string> aFiles, // List<AssetStatus> aOutStatuses) //{ // GetAssetStatuses(aConnection, GetEffectiveFileSpecs(aFiles).ToList(), aOutStatuses); //} /// <summary> /// Using Perforce connection, get a matching AssetStatus for each Assetpath /// </summary> /// <param name="aConnection">Perforce Connection</param> /// <param name="aFiles">A List of asset paths to retrieve</param> /// <returns>A List of AssetStatus, one for each <paramref name="aFiles"/> entry</returns> //static public List<AssetStatus> GetAssetStatusesFromPaths(PerforceConnection aConnection, // List<string> aFiles) //{ // List<AssetStatus> aOutStatuses = new List<AssetStatus>(); // GetAssetStatusesFromPaths(aConnection, aFiles, aOutStatuses); // return aOutStatuses; //} /// <summary> /// 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. /// </summary> /// <param name="aConnection">a valid Perforce Connection</param> /// <param name="aFiles"> A list of FileSpecs to be looked up </param> /// <param name="aOutStatuses">an AssetStatus is added to this list for each <paramref name="aFiles"/> entry</param> //static void GetAssetStatuses(PerforceConnection aConnection, List<FileSpec> aFiles, // List<AssetStatus> aOutStatuses) //{ // IEnumerable<string> assetNames = GetAssetPathsFromFileSpecs(aFiles); // string[] files = assetNames.ToArray(); // Debug.Log("retrieving: " + Logger.StringArrayToString(files)); // List<string> notInitialized = new List<string>(); // // 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)); //} /// <summary> /// Goes to the server, updates cache with latest about the files specified /// Frequently runs in a background thread. /// </summary> /// <param name="aConnection">A valid Perforce connection</param> /// <param name="aFiles">AssetPath list to retrieve info about</param> /// <returns>enumeration of asset paths which were updated</returns> public static IEnumerable<string> GetAssetMetaData(PerforceConnection aConnection, IEnumerable<string> aFiles) { //log.Debug("GetAssetMetaData: " + Logger.StringArrayToString(aFiles.ToArray())); var returnList = new HashSet<string>(); if (aFiles.Any()) { HashSet<string> original = new HashSet<string>(aFiles); HashSet<string> 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<string>(mdlist.ToAssetPaths().EffectivePaths()); } // Remove wildcard entries from original list. // Create Local MetaData entries for files unknown by Perforce IEnumerable<string> 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; } /// <summary> /// We have fresh metadata from the server /// Lets store it in the Asset Cache /// </summary> /// <param name="mdlist">List of Metadata to store</param> public static void StorePerforceMetaData(IList<FileMetaData> 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()); } } /// <summary> /// 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 /// </summary> /// <param name="files">Asset Paths of files to cache</param> public static void StoreLocalMetaData(IEnumerable<string> 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()); } } /// <summary> /// Returns a proper list of file spec given a list of paths, skipping folders, generating folder meta files instead /// </summary> /// <param name="aAssetPaths">Asset Path strings</param> /// <returns>FileSpecs</returns> static IEnumerable<FileSpec> GetEffectiveFileSpecs(IEnumerable<string> aAssetPaths) { foreach (var path in aAssetPaths) { yield return FileSpec.LocalSpec(Utils.AssetPathToLocalPath(Utils.GetEffectivePath(path))); } yield break; } /// <summary> /// Translates a collection of asset paths (including folders) into a collection of LocalSpec FileSpecs /// </summary> /// <param name="aAssetPaths">Asset paths to become FileSpec </param> /// <returns>FileSpecs</returns> public static IEnumerable<FileSpec> GetFileSpecs(IEnumerable<string> aAssetPaths) { foreach (var path in aAssetPaths) { yield return FileSpec.LocalSpec(Utils.AssetPathToLocalPath(path)); } yield break; } /// <summary> /// Return a collection of Asset paths given a list of FileSpecs /// (using the LocalPath of the FileSpec) /// </summary> /// <param name="fs">FileSpecs</param> /// <returns>strings</returns> public static IEnumerable<string> GetAssetPathsFromFileSpecs(IEnumerable<FileSpec> fs) { foreach (var f in fs) { yield return Utils.LocalPathToAssetPath(f.LocalPath.Path); } yield break; } /// <summary> /// Return a collection of Asset paths given a list of FileMetaData /// </summary> /// <param name="fmd_list">FileMetaData collection</param> /// <returns>string collection </returns> public static IEnumerable<string> GetAssetPathsFromFileMetaData(IEnumerable<FileMetaData> 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 /// <summary> /// Encapsulates a thread with an input queue of string arrays. /// Issues "p4 fstat" command for each entry /// </summary> class MetaDataQueue { private Thread _worker; readonly Queue<string[]> _queue = new Queue<string[]>(); 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(); } /// <summary> /// Work - This is run by the background thread. /// It issues a command on the queue /// Then waits for an Event to proceed. /// </summary> private void Work() { while (true) { var argument = new HashSet<string>(); 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 } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 21095 | artofcode |
Populate -o //guest/perforce_software/p4connect/... //guest/artofcode/dev/p4connect/.... |
||
//guest/perforce_software/p4connect/main/src/P4Connect/P4Connect/P4Connect.AssetStatusCache.cs | |||||
#6 | 20275 | Norman Morse |
Update source to match 2016.2 patch 2 release Much thrashing of source during refactor. Proper caching of asset statuses, reducing fstats issued. Some bug fixes by Liz Lam Cleanup of Pending changes, rework of initialization and play/stop play transitions |
||
#5 | 16489 | Norman Morse |
Another pass at async fstats. Removed a UI update on exception |
||
#4 | 16485 | Norman Morse |
Asynchronous status requests Update release notes Minor cleanup |
||
#3 | 16413 | Norman Morse | Code cleanup | ||
#2 | 16350 | Norman Morse |
Minor Code Clean Up Minor Documentation Clean Up Changed Post Processor callback to ignore directories Added option to use Server Typemap |
||
#1 | 16209 | Norman Morse | Move entire source tree into "main" branch so workshop code will act correctly. | ||
//guest/perforce_software/p4connect/src/P4Connect/P4Connect/P4Connect.AssetStatusCache.cs | |||||
#10 | 15383 | Norman Morse |
Improved Diagnostics, cleaned up unnecessary log output Moved some Dialog Initialization to OnEnable() Fixed Unity 5.1.1 incompatibilities Added Operation support for In Depot Deleted files |
||
#9 | 15244 | Norman Morse |
Better Directory support in "add" "get latest" "refresh" and other commands. Improved Project Root detection Various Bug Fixes and Clean up |
||
#8 | 15079 | Norman Morse |
Rewrote AssetStatusCache to Cache AssetStatuses and FileMetaData Fixed Edge conditions on Engine Operations Change Debug output defaults. Will now Checkout files which request to be "added" but which already exist in perforce. Output P4Connect version to log on initialization. |
||
#7 | 14232 | Norman Morse | GA.8 release | ||
#6 | 14193 | Norman Morse |
GA.7 release Refactor Pending Changes Resolve Submit issues. Fixed Menu entries. Handle mismatched file and meta states. |
||
#5 | 13824 | Norman Morse |
Changes to have fstat return true client paths. Remove versions, fixes problems with "local" paths sneaking into results. |
||
#4 | 12945 | Norman Morse | Changes from internal main GA.1 | ||
#3 | 12565 | Norman Morse |
Integrated from Dev Branch Made ChangeManager into Static Class. Improved close window behavior for when connection is invalid Fixed localOpenFiles not updating on submit |
||
#2 | 12553 | Norman Morse |
integrate from internal main Build fixes for EC. Major changes to Configuration and re-initialization code. Bug fixes |
||
#1 | 10940 | Norman Morse |
Inital Workshop release of P4Connect. Released under BSD-2 license |