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 = DateTime.UtcNow;
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();
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));
}
}
///
/// 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 cache
static Dictionary _Statuses;
public static void Initialize()
{
_Statuses = new Dictionary();
}
///
/// Generic method that fetches the cached asset status (checked out, out of date, etc...)
///
public static AssetStatus GetAssetStatus(string aAssetPath)
{
AssetStatus ret = new AssetStatus();
if (_Statuses != null)
{
_Statuses.TryGetValue(GetEffectivePath(aAssetPath), out ret);
}
return ret;
}
///
/// Create an asset status (checked out, out of date, etc...)
/// Faster on multiple files because it batches the P4 requests
/// Guaranteed to have one AssetStatus per AssetPath
///
public static void GetAssetData(PerforceConnection aConnection, List aAssetPaths, List aOutStatuses)
{
if (! Config.ValidConfiguration)
return;
DateTime startTimestamp = DateTime.Now;
List localFiles = GetFileSpecs(aAssetPaths);
GetAssetData(aConnection, localFiles, aOutStatuses);
// Cache the AssetStatuses in the _Statuses dictionary
for (int i = 0; i < aAssetPaths.Count; ++i)
{
_Statuses[GetEffectivePath(aAssetPaths[i])] = aOutStatuses[i];
}
if (Config.DisplayP4Timings)
{
double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds;
aConnection.AppendTimingInfo("GetAssetData " + deltaInnerTime.ToString() + " ms for " + aAssetPaths.Count + " files (" + aOutStatuses.Count + " retrieved)");
}
}
///
/// Force P4Connect to update the status of a file in its status cache
///
/// List of Linux style Project Relative Paths
public static void UpdateAssetData(List aAssetPaths)
{
if (! Config.ValidConfiguration)
return;
//Debug.Log("paths: " + Logger.StringArrayToString(aAssetPaths.ToArray()));
List localFiles = GetFileSpecs(aAssetPaths);
List statuses = new List();
//Debug.Log("Filespecs: " + Logger.FileSpecListToString(localFiles));
Engine.PerformConnectionOperation(con =>
{
GetAssetData(con, localFiles, statuses);
});
if (statuses.Count == aAssetPaths.Count)
{
for (int i = 0; i < aAssetPaths.Count; ++i)
{
_Statuses[GetEffectivePath(aAssetPaths[i])] = statuses[i];
}
}
}
///
/// 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);
}
}
allSpecs = allSpecs.ValidSpecs().ToList();
List allStatuses = new List();
GetAssetData(aConnection, allSpecs, allStatuses);
// Cache the returned values
for (int i = 0; i < allSpecs.Count; ++i)
{
string path = allSpecs[i].ToAssetPath(aConnection);
if (! string.IsNullOrEmpty(path))
{
_Statuses[GetEffectivePath(path)] = allStatuses[i];
#if DEBUG
log.Debug("_Statuses[" + GetEffectivePath(path) + "] = " + allStatuses[i].ToString());
#endif
}
}
if (_OnAssetStatusChanged != null)
{
_OnAssetStatusChanged(aConnection);
}
}
}
///
/// 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 GetAssetData(PerforceConnection aConnection, List aFiles, List aOutStatuses)
{
//DateTime startTimestamp = DateTime.Now;
if (aFiles.Count > 0)
{
Options fsopts = new Options();
fsopts["-Op"] = null; // Return "real" clientpaths
//Debug.Log("GetAssetData: " + Logger.FileSpecListToString(aFiles));
Dictionary dict = new Dictionary();
// Create a Dictionary from any non-null response.
var mdlist = Engine.GetFileMetaData(aConnection, aFiles, fsopts);
if (mdlist != null)
{
//Debug.Log("AssetData: " + Logger.FileMetaDataListToString(mdlist));
foreach(var md in mdlist)
{
dict.Add(md.LocalPath.Path, md);
}
}
//if (Config.DisplayP4Timings)
//{
// double deltaInnerTime = (DateTime.Now - matchingMetaStartTime).TotalMilliseconds;
// aConnection.AppendTimingInfo("GetMatchingMetaData " + deltaInnerTime.ToString() + " ms");
//}
foreach( FileSpec f in aFiles)
{
AssetStatus status;
FileMetaData md = new FileMetaData();
if (dict.TryGetValue(f.LocalPath.Path, out md))
{
status = CreateAssetStatus(md);
}
else
{
status = new AssetStatus();
}
aOutStatuses.Add(status);
}
}
//Debug.Log("GetAssetData: filecount " + aFiles.Count.ToString() + " statuscount: " + aOutStatuses.Count.ToString());
//Debug.Log("GetAssetData: content " + Logger.AssetStatusListToString(aOutStatuses));
//if (Config.DisplayP4Timings)
//{
// double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds;
// aConnection.AppendTimingInfo("GetAssetData " + deltaInnerTime.ToString() + " ms");
//}
}
///
/// Returns a proper list of file spec given a list of paths, accounting for directories
///
static List GetFileSpecs(List aAssetPaths)
{
List localFiles = new List();
foreach (var path in aAssetPaths)
{
localFiles.Add(FileSpec.LocalSpec(Utils.AssetPathToLocalPath(GetEffectivePath(path))));
}
return localFiles;
}
///
/// 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(aAssetPath);
}
else
{
return aAssetPath;
}
}
}
}