using UnityEditor;
using UnityEngine;
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Perforce.P4;
using log4net;
namespace P4Connect
{
///
/// Stores and periodically updates the Perforce status of all the assets
///
public class AssetStatusCache
{
private static readonly ILog log = LogManager.GetLogger(typeof(AssetStatusCache));
///
/// Simple asset status struct
/// Stores the file state (checked out, modified, etc...) and revision state
///
public struct AssetStatus
{
public DateTime TimeStamp;
public FileState LocalState;
public FileState OtherState;
public DepotState DepotState;
public RevisionState RevisionState;
public ResolvedState ResolvedState;
public StorageType StorageType;
public LockState LockState;
// Use this to create an AssetStatus for a local file (perforce would return null from the fstat call)
public AssetStatus(string assetPath)
{
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;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
if (TimeStamp == default(DateTime))
{
sb.AppendFormat("Dummy AssetStatus");
}
else
{
sb.AppendFormat("TimeStamp: {0} ", TimeStamp);
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 IsLocked()
{
return (LockState == LockState.OurLock || LockState == LockState.TheirLock);
}
public bool IsOpened()
{
return (! (LocalState == FileState.None || LocalState == FileState.InDepot));
}
public bool IsInitialized()
{
return (this.TimeStamp != default(DateTime));
}
}
///
/// 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 AssetEntry()
{
status = new AssetStatus();
meta = new FileMetaData();
status.TimeStamp = update = default(DateTime);
}
public bool Dirty {
get {return (update == default(DateTime)); }
set {
if (value == true){
status.TimeStamp = update = default(DateTime);
}
}
}
}
///
/// 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;
public static void Initialize()
{
_Entries = new Dictionary();
PendingDirty = true;
}
///
/// 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 = GetEffectivePath(aAssetPath);
if (! _Entries.TryGetValue(path, out entry))
{
//log.Debug("Creating DEFAULT: " + path);
entry = new AssetEntry();
_Entries[path] = entry;
}
return entry;
}
public static AssetStatus GetAssetStatus(string aAssetPath)
{
AssetEntry entry = GetAssetEntry(aAssetPath);
return entry.status;
}
public static FileMetaData GetMetaData(string aAssetPath)
{
AssetEntry entry = GetAssetEntry(aAssetPath);
return entry.meta;
}
public static FileMetaData GetMetaData(FileSpec spec)
{
string path = Utils.LocalPathToAssetPath(spec.LocalPath.Path);
AssetEntry entry = GetAssetEntry(path);
return entry.meta;
}
public static DateTime GetUpdated(string aAssetPath)
{
AssetEntry entry = GetAssetEntry(aAssetPath);
return entry.update;
}
// A flag for pending changes to check if something has changedIgnore file name
public static bool PendingDirty { get; set; }
///
/// Called from the Icons code.
/// update the status of each file in the list
/// Check for cached entries, if any are not, ask the server for them.
///
/// List of Asset Paths
public static void UpdateAssetData(List aAssetPaths)
{
if (! Config.ValidConfiguration)
return;
// Remember the list
HashSet assets = new HashSet(aAssetPaths);
// Check each asset for a valid status
foreach(string asset in aAssetPaths)
{
AssetStatus status = GetAssetStatus(asset);
if (status.IsInitialized())
{
assets.Remove(asset);
}
}
if (assets.Any())
{
// Get any left-overs from the server
Engine.PerformConnectionOperation(con =>
{
UpdateAssetMetaData(con, GetFileSpecs(assets).ToList());
});
}
}
///
/// Given the meta data, figures out the P4Connect icon representation for the file
/// There is only a subset of all the metadata that P4Connect actually displays
///
static AssetStatus CreateAssetStatus(FileMetaData aMeta)
{
// Debug.Log("CreateAssetStatus: " + Logger.FileMetaDataToString(aMeta));
AssetStatus retData = new AssetStatus();
if (aMeta == null)
return retData;
// Fill out the struct
switch (aMeta.Type.BaseType)
{
case BaseFileType.Text:
retData.StorageType = StorageType.Text;
break;
case BaseFileType.Binary:
retData.StorageType = StorageType.Binary;
break;
default:
retData.StorageType = StorageType.Other;
break;
}
retData.LocalState = Engine.ParseFileAction(aMeta.Action);
if (retData.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)
retData.LocalState = FileState.InDepot;
}
if (aMeta.OtherActions != null)
{
retData.OtherState = Engine.ParseFileAction(aMeta.OtherActions[0]);
}
if (aMeta.HaveRev >= aMeta.HeadRev)
retData.RevisionState = RevisionState.HasLatest;
else
retData.RevisionState = RevisionState.OutOfDate;
if (aMeta.Unresolved)
retData.ResolvedState = ResolvedState.NeedsResolve;
else
retData.ResolvedState = ResolvedState.None;
if (aMeta.HeadAction == FileAction.MoveDelete || aMeta.HeadAction == FileAction.Delete)
retData.DepotState = DepotState.Deleted;
else
retData.DepotState = DepotState.None;
retData.TimeStamp = DateTime.Now;
retData.LockState = Engine.GetLockState(aMeta);
// Debug.Log("AssetStatus: " + retData.ToString());
return retData;
}
///
/// This functions gets called when P4Connect does *something* to files
/// In this case we update the files status
///
static void OnEngineOperationPerformed(PerforceConnection aConnection, List aFilesAndMetas)
{
if (aFilesAndMetas.Count > 0)
{
List allSpecs = new List();
foreach (var fileAndMeta in aFilesAndMetas)
{
if (fileAndMeta.File != null)
{
allSpecs.Add(fileAndMeta.File);
}
if (fileAndMeta.Meta != null)
{
allSpecs.Add(fileAndMeta.Meta);
}
}
//log.Debug("files changed: " + Logger.FileAndMetaListToString(aFilesAndMetas));
// Only update entries with version numbers
allSpecs = allSpecs.ValidSpecs().ToList();
// But we can't pass the versions to fstat
UpdateAssetMetaData(aConnection, allSpecs.UnversionedSpecs().ToList()); // Refresh the cache
if (_OnAssetStatusChanged != null)
{
_OnAssetStatusChanged(aConnection);
}
}
}
///
/// Given a collection of folders and paths, refresh cache information
///
///
static public void Refresh(IEnumerable paths)
{
IEnumerable paths1 = paths.AddDirectoryWildcards();
Engine.PerformConnectionOperation(con =>
{
UpdateAssetMetaData(con, GetFileSpecs(paths1).ToList());
});
}
///
/// Given a folder, invalidate all cache data for members
///
/// Path to dirty folder
static public 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.
///
///
static public 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
static public 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 statuses, one per aFile<
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.
///
/// Perforce Connection
/// Files to look up
/// AssetStatus List returned, one for each File
static void GetAssetStatuses(PerforceConnection aConnection, List aFiles, List aOutStatuses)
{
IEnumerable asset_names = GetAssetPathsFromFileSpecs(aFiles);
string[] files = asset_names.ToArray();
//Debug.Log("retrieving: " + Logger.StringArrayToString(files));
List not_initialized = 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] = GetAssetStatus(files[i]);
if (! stats[i].IsInitialized())
{
not_initialized.Add(files[i]);
}
}
// Now Ask for any uninitialized AssetStatus's from the server
if (not_initialized.Count > 0)
{
log.Debug("Going to server for: " + Logger.StringArrayToString(not_initialized.ToArray()));
Engine.PerformConnectionOperation(con =>
{
UpdateAssetMetaData(aConnection, GetFileSpecs(not_initialized).ToList());
});
}
// Finally update the "holes" in the statuses originally returned
for (int i = 0; i < files.Count(); i++)
{
if (!stats[i].IsInitialized())
{
stats[i] = GetAssetStatus(files[i]);
}
}
aOutStatuses.AddRange(stats);
//log.Debug("assetstatuses: " + Logger.AssetStatusListToString(aOutStatuses));
}
///
/// Goes to the server, updates cache with latest about the files specified
///
/// Connection to use
/// Files to retrieve info about
static void UpdateAssetMetaData(PerforceConnection aConnection, List aFiles)
{
if (aFiles != null && aFiles.Count > 0)
{
aFiles = aFiles.UnversionedSpecs().ToList();
//log.Debug("update metadata: " + Logger.FileSpecListToString(aFiles));
HashSet original = new HashSet(GetAssetPathsFromFileSpecs(aFiles));
HashSet checked_list = new HashSet();
DateTime startTimestamp = DateTime.Now;
Options fsopts = new Options();
fsopts["-Op"] = null; // Return "real" clientpaths
var mdlist = Engine.GetFileMetaData(aConnection, aFiles, fsopts);
if (mdlist != null)
{
// with a good response, we store the retrieved meta data in the AssetCache
StorePerforceMetaData(mdlist);
foreach (var md in mdlist)
{
checked_list.Add(GetEffectivePath(Utils.LocalPathToAssetPath(md.LocalPath.Path)));
}
}
// For "leftover" files not recognized by the server, we provide an updated AssetEntry
IEnumerable left_overs = original.Except(checked_list);
StoreLocalMetaData(left_overs);
if (Config.DisplayP4Timings)
{
double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds;
aConnection.AppendTimingInfo("UpdateAssetMetaData " + deltaInnerTime.ToString() + " ms");
}
}
}
///
/// We have fresh metadata from the server
/// Lets store it in the Asset Cache
///
/// List of Metadata to store
static public 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 = GetEffectivePath(Utils.LocalPathToAssetPath(md.LocalPath.Path));
AssetEntry entry = new AssetEntry();
entry.meta = md;
entry.status = CreateAssetStatus(md);
entry.update = DateTime.Now;
_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
static public 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;
_Entries[f] = entry;
//log.Debug("AssetEntry: " + f + " CREATED 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(GetEffectivePath(path)));
}
yield break;
}
///
/// Translates a collection of asset paths (including folders) into a collection of FileSpecs
///
/// paths to translate
/// FileSpecs returned
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
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
static IEnumerable GetAssetPathsFromFileMetaData(IEnumerablefmd_list)
{
foreach(var fm in fmd_list)
{
yield return Utils.LocalPathToAssetPath(fm.LocalPath.Path);
}
yield break;
}
///
/// Returns the path to use when printing the name, or looking up status info
///
static string GetEffectivePath(string aAssetPath)
{
if (Utils.IsDirectory(aAssetPath))
{
return Utils.MetaFromAsset(Utils.RemoveDirectoryWildcards(aAssetPath));
}
else
{
return aAssetPath;
}
}
}
}