public void Setup(PolyCacheConfig config) { // Create all the buffer holders. They are all initially idle. for (int i = 0; i < MAX_CONCURRENT_DOWNLOADS; i++) { idleBuffers.Add(new BufferHolder()); } // Caching is only supported on Windows/Mac for now. bool cacheSupported = Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer; PtDebug.LogFormat("Platform: {0}, cache supported: {1}", Application.platform, cacheSupported); if (cacheSupported && config.cacheEnabled) { //string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); //string defaultCachePath = Path.Combine(Path.Combine(Path.Combine( // appDataPath, Application.companyName), Application.productName), "WebRequestCache"); string defaultCachePath = Path.Combine(Application.persistentDataPath, "WebRequestCache"); string cachePath = config.cachePathOverride; if (string.IsNullOrEmpty(cachePath)) { cachePath = defaultCachePath; } // Note: Directory.CreateDirectory creates all directories in the path. Directory.CreateDirectory(cachePath); cache = gameObject.AddComponent <PersistentBlobCache>(); cache.Setup(cachePath, config.maxCacheEntries, config.maxCacheSizeMb * 1024 * 1024); } }
/// <summary> /// Checks for pending deliveries and delivers them. /// </summary> private void Update() { if (!setupDone) { return; } // To avoid locking the queue on every frame, exit early if the volatile count is 0. if (requestsPendingDelivery.VolatileCount == 0) { return; } // Check for a pending delivery. // Note that for performance reasons, we limit ourselves to delivering one result per frame. CacheRequest delivery; if (!requestsPendingDelivery.Dequeue(out delivery)) { return; } PtDebug.LogVerboseFormat("PBC: delivering result on {0} ({1}, {2} bytes).", delivery.hash, delivery.success ? "SUCCESS" : "FAILURE", delivery.data != null ? delivery.data.Length : -1); // Deliver the results to the callback. delivery.readCallback(delivery.success, delivery.data); // Recycle the request for reuse. delivery.Reset(); requestsRecyclePool.Enqueue(delivery); }
/// <summary> /// Called when sign in finishes. /// </summary> /// <param name="wasInteractive">If true, this was the interactive (browser-based) sign-in flow.</param> /// <param name="status">The result of the sign in process.</param> private void OnSignInFinished(bool wasInteractive, PolyStatus status) { if (status.ok) { string tok = PolyApi.AccessToken; PtDebug.LogFormat("ABM: Sign in success. Access token: {0}", (tok != null && tok.Length > 6) ? tok.Substring(0, 6) + "..." : "INVALID"); PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_IN_SUCCESS); } else if (wasInteractive) { Debug.LogErrorFormat("Failed to sign in. Please try again: " + status); PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_IN_FAILURE, status.ToString()); } if (null != refreshCallback) { refreshCallback(); } // If we had a deferred request that was waiting for auth, send it now. if (requestToSendAfterAuth != null) { PtDebug.Log("Sending deferred request that was waiting for auth."); PolyRequest request = requestToSendAfterAuth; requestToSendAfterAuth = null; StartRequest(request); } }
/// <summary> /// Sets up a cache with the given characteristics. /// </summary> /// <param name="rootPath">The absolute path to the root of the cache.</param> /// <param name="maxEntries">The maximum number of entries in the cache.</param> /// <param name="maxSizeBytes">The maximum combined size of all entries in the cache.</param> public void Setup(string rootPath, int maxEntries, long maxSizeBytes) { this.rootPath = rootPath; this.maxEntries = maxEntries; this.maxSizeBytes = maxSizeBytes; // Check that we have a reasonable config: PolyUtils.AssertNotNullOrEmpty(rootPath, "rootPath can't be null or empty"); PolyUtils.AssertTrue(Directory.Exists(rootPath), "rootPath must be an existing directory: " + rootPath); PolyUtils.AssertTrue(maxEntries >= 256, "maxEntries must be >= 256"); PolyUtils.AssertTrue(maxSizeBytes >= 1048576, "maxSizeBytes must be >= 1MB"); PtDebug.LogVerboseFormat("PBC initializing, root {0}, max entries {1}, max size {2}", rootPath, maxEntries, maxSizeBytes); md5 = MD5.Create(); InitializeCache(); setupDone = true; Thread backgroundThread = new Thread(BackgroundThreadMain); backgroundThread.IsBackground = true; backgroundThread.Start(); }
/// <summary> /// Callback invoked when we get request results. /// </summary> private void OnRequestResult(PolyStatusOr <PolyListAssetsResult> result) { if (result.Ok) { PtDebug.LogFormat("ABM: request results received ({0} assets).", result.Value.assets.Count); this.listAssetsResult = result; resultHasMorePages = result.Value.nextPageToken != null; } else { Debug.LogError("Asset request failed. Try again later: " + result.Status); this.listAssetsResult = result; } querying = false; if (null != refreshCallback) { refreshCallback(); } if (result.Ok) { assetsInUse = GetAssetsInUse(); FinishFetchingThumbnails(result); } }
/// <summary> /// (Background thread). Handles a write request. Writes the data to disk. /// </summary> /// <param name="writeRequest">The write request to execute.</param> private void BackgroundHandleWriteRequest(CacheRequest writeRequest) { PtDebug.LogVerboseFormat("PBC: executing write request for {0}", writeRequest.hash); string fullPath = HashToFullPath(writeRequest.hash); string tempPath = Path.Combine(Path.GetDirectoryName(fullPath), "temp.dat"); // In the event of a crash or hardware issues -- e.g., user trips on the power cord, our write // to disk might be interrupted in an inconsistent state. So instead of writing directly to // the destination file, we write to a temporary file and then move. File.WriteAllBytes(tempPath, writeRequest.data); if (File.Exists(fullPath)) { File.Delete(fullPath); } File.Move(tempPath, fullPath); // Update the file size and last used time information in the cache. CacheEntry entry; if (!cacheEntries.TryGetValue(writeRequest.hash, out entry)) { entry = cacheEntries[writeRequest.hash] = new CacheEntry(writeRequest.hash); } entry.fileSize = writeRequest.data.Length; entry.writeTimestampMillis = TicksToMillis(DateTime.UtcNow.Ticks); // We are done with writeRequest, so we can recycle it. writeRequest.Reset(); requestsRecyclePool.Enqueue(writeRequest); // Check if the cache needs trimming. TrimCache(); }
public AssetBrowserManager() { PtDebug.Log("ABM initializing..."); EnsurePolyIsReady(); // Initially, show the featured assets home page. StartRequest(PolyListAssetsRequest.Featured()); }
/// <summary> /// Clears the current request. Also cancels any pending request. /// </summary> public void ClearRequest() { PtDebug.Log("ABM: clearing request..."); querying = false; // Increasing the ID will cause us to ignore the results of any pending requests // (we will know they are obsolete by their query ID). queryId++; listAssetsResult = null; resultHasMorePages = false; }
/// <summary> /// (Background thread). Clears the entire cache. /// </summary> private void BackgroundHandleClearRequest(CacheRequest clearRequest) { PtDebug.LogVerboseFormat("Clearing the cache."); foreach (string file in Directory.GetFiles(rootPath, "*" + BLOB_FILE_EXT)) { File.Delete(file); } cacheEntries.Clear(); clearRequest.Reset(); requestsRecyclePool.Enqueue(clearRequest); }
/// <summary> /// Requests that the cache be cleared. The cache will be cleared asynchronously. /// </summary> public void RequestClear() { CacheRequest request; if (!requestsRecyclePool.Dequeue(out request)) { request = new CacheRequest(); } request.type = RequestType.CLEAR; PtDebug.LogVerboseFormat("PBC: enqueing CLEAR request."); requestsPendingWork.Enqueue(request); }
/// <summary> /// Get the next page of assets from the current request. /// </summary> public void GetNextPageRequest() { PtDebug.Log("ABM: getting next page of current request..."); if (CurrentResult == null || !CurrentResult.Ok) { Debug.LogError("Request failed, no valid current result to get next page of."); } currentRequest.pageToken = CurrentResult.Value.nextPageToken; StartRequest(currentRequest, OnNextPageRequestResult); }
/// <summary> /// Initializes the cache (reads the cache state from disk). /// </summary> private void InitializeCache() { foreach (string file in Directory.GetFiles(rootPath)) { if (file.EndsWith(BLOB_FILE_EXT)) { FileInfo finfo = new FileInfo(file); string hash = Path.GetFileNameWithoutExtension(file).ToLowerInvariant(); cacheEntries[hash] = new CacheEntry(hash, finfo.Length, TicksToMillis(finfo.LastWriteTimeUtc.Ticks)); PtDebug.LogVerboseFormat("PBC: loaded existing cache item: {0} => {1} bytes", hash, cacheEntries[hash].fileSize); } } }
private bool PrepareDownload(PolyAsset asset, out string baseName, out string downloadLocalPath) { assetsBeingDownloaded.Remove(asset); PtDebug.LogFormat("ABM: Preparing to download {0}", asset); // basePath is something like Assets/Poly/Sources. string baseLocalPath = PtUtils.NormalizeLocalPath(PtSettings.Instance.assetSourcesPath); if (!baseLocalPath.StartsWith("Assets/")) { Debug.LogErrorFormat("Invalid asset sources folder {0}. Must be under Assets folder."); baseName = downloadLocalPath = null; return(false); } // basePathAbs is something like C:\Users\foo\bar\MyUnityProject\Assets\Poly\Sources string baseFullPath = PtUtils.ToAbsolutePath(baseLocalPath); if (!Directory.Exists(baseFullPath)) { Directory.CreateDirectory(baseFullPath); } baseName = PtUtils.GetPtAssetBaseName(asset); PtDebug.LogFormat("Import name: {0}", baseName); // downloadLocalPath is something like Assets/Poly/Sources/assetTitle_assetId downloadLocalPath = baseLocalPath + "/" + baseName; string downloadFullPath = PtUtils.ToAbsolutePath(downloadLocalPath); if (Directory.Exists(downloadFullPath)) { if (PtSettings.Instance.warnOnSourceOverwrite && !EditorUtility.DisplayDialog("Warning: Overwriting asset source folder", string.Format("The asset source folder '{0}' will be deleted and created again. " + "This should be safe *unless* you have manually made changes to its contents, " + "in which case you will lose those changes.\n\n" + "(You can silence this warning in Poly Toolkit settings)", asset.displayName, downloadLocalPath), "OK", "Cancel")) { return(false); } Directory.Delete(downloadFullPath, /* recursive */ true); } // Create the download folder. // Something like C:\Users\foo\bar\MyUnityProject\Assets\Poly\Sources\assetTitle_assetId Directory.CreateDirectory(downloadFullPath); return(true); }
/// <summary> /// Parses a single asset. /// </summary> public static PolyStatus ParseAsset(JObject asset, out PolyAsset polyAsset) { polyAsset = new PolyAsset(); if (asset["visibility"] == null) { return(PolyStatus.Error("Asset has no visibility set.")); } polyAsset.name = asset["name"].ToString(); polyAsset.authorName = asset["authorName"].ToString(); if (asset["thumbnail"] != null) { var thumbnailElements = asset["thumbnail"].ToObject <JObject>(); //IJEnumerable<JToken> thumbnailElements = asset["thumbnail"].AsJEnumerable(); polyAsset.thumbnail = new PolyFile(thumbnailElements["relativePath"].ToString(), thumbnailElements["url"].ToString(), thumbnailElements["contentType"].ToString()); } if (asset["formats"] == null) { Debug.LogError("No formats found"); } else { foreach (var format in asset["formats"].ToObject <List <JObject> >()) //foreach (JToken format in asset["formats"]) { PolyFormat newFormat = ParseAssetsPackage(format); newFormat.formatType = ParsePolyFormatType(format["formatType"]); if (newFormat.formatType == PolyFormatType.UNKNOWN) { PtDebug.Log("Did not recognize format type: " + format["formatType"].ToString()); } polyAsset.formats.Add(newFormat); } } polyAsset.displayName = asset["displayName"].ToString(); polyAsset.createTime = DateTime.Parse(asset["createTime"].ToString()); polyAsset.updateTime = DateTime.Parse(asset["updateTime"].ToString()); polyAsset.visibility = ParsePolyVisibility(asset["visibility"]); polyAsset.license = ParsePolyAssetLicense(asset["license"]); if (asset["isCurated"] != null) { polyAsset.isCurated = bool.Parse(asset["isCurated"].ToString()); } return(PolyStatus.Success()); }
/// <summary> /// Requests a write to the cache. The data will be written asynchronously. /// </summary> /// <param name="key">The key to write.</param> /// <param name="data">The data to write.</param> public void RequestWrite(string key, byte[] data) { string hash = GetHash(key); CacheRequest request; if (!requestsRecyclePool.Dequeue(out request)) { request = new CacheRequest(); } request.type = RequestType.WRITE; request.key = key; request.hash = hash; request.data = data; PtDebug.LogVerboseFormat("PBC: enqueing WRITE request for {0}", key); requestsPendingWork.Enqueue(request); }
private void TrimCache() { long totalSize = 0; foreach (CacheEntry cacheEntry in cacheEntries.Values) { totalSize += cacheEntry.fileSize; } if (totalSize <= maxSizeBytes && cacheEntries.Count <= maxEntries) { // We're within budget, no need to trim the cache. return; } // Sort the entries from oldest to newest. This is the order in which we will evict them. Queue <CacheEntry> entriesOldestToNewest = new Queue <CacheEntry>(cacheEntries.Values.OrderBy(entry => entry.writeTimestampMillis)); // Each iteration evicts the oldest item, until we're back under budget. while (totalSize > maxSizeBytes || cacheEntries.Count > maxEntries) { PtDebug.LogVerboseFormat("PBC: trimming cache, bytes {0}/{1}, entries {2}/{3}", totalSize, maxSizeBytes, cacheEntries.Values.Count, maxEntries); // What's the oldest file? if (entriesOldestToNewest.Count == 0) { break; } CacheEntry oldest = entriesOldestToNewest.Dequeue(); // Delete this file. string filePath = HashToFullPath(oldest.hash); if (File.Exists(filePath)) { File.Delete(filePath); } cacheEntries.Remove(oldest.hash); // Update our accounting totalSize -= oldest.fileSize; } PtDebug.LogVerboseFormat("PBC: end of trim, bytes {0}/{1}, entries {2}/{3}", totalSize, maxSizeBytes, cacheEntries.Count, maxEntries); }
/// <summary> /// Requests a read from the cache. /// </summary> /// <param name="key">The key to read.</param> /// <param name="maxAgeMillis">Maximum age for a cache hit. If the copy we have on cache is older /// than this, the request will fail. Use -1 to mean "any age".</param> /// <param name="callback">The callback that is to be called (asynchronously) when the read operation /// finishes. This callback will be called on the MAIN thread.</param> public void RequestRead(string key, long maxAgeMillis, CacheReadCallback callback) { string hash = GetHash(key); CacheRequest request; if (!requestsRecyclePool.Dequeue(out request)) { request = new CacheRequest(); } request.type = RequestType.READ; request.key = key; request.hash = hash; request.readCallback = callback; request.maxAgeMillis = maxAgeMillis; PtDebug.LogVerboseFormat("PBC: enqueing READ request for {0}", key); requestsPendingWork.Enqueue(request); }
/// <summary> /// Because Poly doesn't live in the Editor/ space (and couldn't, since it uses GameObjects and /// MonoBehaviours), it will die every time the user enters or exits play mode. This means /// that all of its state and objects will get wiped. So we have to check if it needs initialization /// every time we need to use it. /// </summary> public void EnsurePolyIsReady() { if (!PolyApi.IsInitialized) { PtDebug.Log("ABM: Initializing Poly."); // We need to set a service name for our auth config because we want to keep our auth credentials // separate in a different "silo", so they don't get confused with the runtime credentials // the user might be using in their project. Regular users would not set a service name, so they // use the default silo. authConfig.serviceName = "PolyToolkitEditor"; PolyApi.Init(authConfig, cacheConfig); waitingForSilentAuth = true; //PolyApi.Authenticate(interactive: false, callback: (PolyStatus status) => { // waitingForSilentAuth = false; // OnSignInFinished(/* wasInteractive */ false, status); //}); } }
/// <summary> /// Starts downloading and importing the given asset (in the background). When done, the asset will /// be saved to the user's Assets folder. /// </summary> /// <param name="asset">The asset to download and import.</param> /// <param name="ptAssetLocalPath">Path to the PtAsset that should be created (or replaced).</param> /// <param name="options">Import options.</param> public void StartDownloadAndImport(PolyAsset asset, string ptAssetLocalPath, EditTimeImportOptions options) { if (!assetsBeingDownloaded.Add(asset)) { return; } PtDebug.LogFormat("ABM: starting to fetch asset {0} ({1}) -> {2}", asset.name, asset.displayName, ptAssetLocalPath); // Prefer glTF1 to glTF2. // It used to be that no Poly assets had both formats, so the ordering did not matter. // Blocks assets now have both glTF1 and glTF2. PT does not understand the glTF2 version, // so the ordering matters a great deal. PolyFormat glTF2format = asset.GetFormatIfExists(PolyFormatType.GLTF_2); PolyFormat glTFformat = asset.GetFormatIfExists(PolyFormatType.GLTF); PolyMainInternal.FetchProgressCallback progressCallback = (PolyAsset assetBeingFetched, float progress) => { EditorUtility.DisplayProgressBar(DOWNLOAD_PROGRESS_TITLE, DOWNLOAD_PROGRESS_TEXT, progress); }; if (glTFformat != null) { EditorUtility.DisplayProgressBar(DOWNLOAD_PROGRESS_TITLE, DOWNLOAD_PROGRESS_TEXT, 0.0f); PolyMainInternal.Instance.FetchFormatFiles(asset, PolyFormatType.GLTF, (PolyAsset resultAsset, PolyStatus status) => { EditorUtility.ClearProgressBar(); OnFetchFinished(status, resultAsset, /*isGltf2*/ false, ptAssetLocalPath, options); }, progressCallback); } else if (glTF2format != null) { EditorUtility.DisplayProgressBar(DOWNLOAD_PROGRESS_TITLE, DOWNLOAD_PROGRESS_TEXT, 0.0f); PolyMainInternal.Instance.FetchFormatFiles(asset, PolyFormatType.GLTF_2, (PolyAsset resultAsset, PolyStatus status) => { EditorUtility.ClearProgressBar(); OnFetchFinished(status, resultAsset, /*isGltf2*/ true, ptAssetLocalPath, options); }, progressCallback); } else { Debug.LogError("Asset not in GLTF_2 or GLTF format. Can't import."); PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_FAILED, "Unsupported format"); } }
public AssetBrowserManager(PolyRequest request) { PtDebug.Log("ABM initializing..."); EnsurePolyIsReady(); // If this is a request that needs authentication and we are in the process of authenticating, // wait until we're finished. bool needAuth = request is PolyListLikedAssetsRequest || request is PolyListUserAssetsRequest; if (needAuth && waitingForSilentAuth) { // Defer the request. Wait until auth is complete. PtDebug.Log("ABM: Deferring request until after auth."); requestToSendAfterAuth = request; return; } StartRequest(request); }
private void OnFetchFinished(PolyStatus status, PolyAsset asset, bool isGltf2, string ptAssetLocalPath, EditTimeImportOptions options) { if (!status.ok) { Debug.LogErrorFormat("Error fetching asset {0} ({1}): {2}", asset.name, asset.displayName, status); EditorUtility.DisplayDialog("Download Error", string.Format("*** Error downloading asset '{0}'. Try again later.", asset.displayName), "OK"); PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_FAILED, "Asset fetch failed"); return; } string baseName, downloadLocalPath; if (!PrepareDownload(asset, out baseName, out downloadLocalPath)) { return; } string absPath = PtUtils.ToAbsolutePath(downloadLocalPath); string extension = isGltf2 ? ".gltf2" : ".gltf"; string fileName = baseName + extension; // We have to place an import request so that PolyImporter does the right thing when it sees the new file. PolyImporter.AddImportRequest(new PolyImporter.ImportRequest( downloadLocalPath + "/" + fileName, ptAssetLocalPath, options, asset)); // Now unpackage it. GltfProcessor will pick it up automatically. UnpackPackageToFolder(isGltf2 ? asset.GetFormatIfExists(PolyFormatType.GLTF_2) : asset.GetFormatIfExists(PolyFormatType.GLTF), absPath, fileName); PtDebug.LogFormat("ABM: Successfully downloaded {0} to {1}", asset, absPath); AssetDatabase.Refresh(); if (null != refreshCallback) { refreshCallback(); } }
/// <summary> /// Callback invoked when we receive the result of a request for a specific asset. /// </summary> private void OnRequestForSpecificAssetResult(PolyStatusOr <PolyAsset> result) { if (result.Ok) { PtDebug.Log("ABM: get asset request received result."); assetResult = result.Value; if (!thumbnailCache.TryGet(assetResult.name, out assetResult.thumbnailTexture)) { PolyApi.FetchThumbnail(assetResult, OnThumbnailFetched); } } else { Debug.LogError("Error: " + result.Status.errorMessage); } querying = false; if (null != refreshCallback) { refreshCallback(); } }
/// <summary> /// (Background thread). Handles a read request, reading it from disk and scheduling the delivery /// of the results to the caller. /// </summary> /// <param name="readRequest">The read request to execute.</param> private void BackgroundHandleReadRequest(CacheRequest readRequest) { PtDebug.LogVerboseFormat("PBC: executing read request for {0} ({1})", readRequest.key, readRequest.hash); string fullPath = HashToFullPath(readRequest.hash); CacheEntry entry; if (!cacheEntries.TryGetValue(readRequest.hash, out entry)) { // Not in the cache readRequest.data = null; readRequest.success = false; } else if (readRequest.maxAgeMillis > 0 && entry.AgeMillis > readRequest.maxAgeMillis) { // Too old. readRequest.data = null; readRequest.success = false; } else if (!File.Exists(fullPath)) { // Too old. readRequest.data = null; readRequest.success = false; } else { // Found it. readRequest.data = File.ReadAllBytes(fullPath); readRequest.success = true; // Update the read timestamp. entry.readTimestampMillis = TicksToMillis(DateTime.UtcNow.Ticks); } // Schedule the result for delivery to the caller. requestsPendingDelivery.Enqueue(readRequest); }
/// <summary> /// Sends a hit to Google Analytics. /// </summary> /// <param name="fields">Key-value pairs that make up the properties of the hit. These /// are assumed to be in the appropriate Google Analytics format.</param> private void SendHit(Dictionary <string, string> fields) { StringBuilder sb = new StringBuilder(prefix); foreach (KeyValuePair <string, string> pair in fields) { sb.AppendFormat("&{0}={1}", UnityWebRequest.EscapeURL(pair.Key), UnityWebRequest.EscapeURL(pair.Value)); } string payload = sb.ToString(); try { UnityWebRequest request = new UnityWebRequest("https://www.google-analytics.com/collect", "POST"); request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(payload)); request.uploadHandler.contentType = "application/x-www-form-urlencoded"; UnityCompat.SendWebRequest(request); PtDebug.LogFormat("ANALYTICS: sent hit: {0}", payload); } catch (Exception ex) { // Reporting these as errors would be noisy and annoying. We don't want to do that -- maybe the user is // offline. Not being able to send analytics isn't a big deal. So only log this error if // PtDebug verbose logging is on. PtDebug.LogFormat("*** Error sending analytics: {0}", ex); } }
/// <summary> /// Co-routine that services one PendingRequest. This method must be called with StartCoroutine. /// </summary> /// <param name="request">The request to service.</param> private IEnumerator HandleWebRequest(PendingRequest request, BufferHolder bufferHolder) { // NOTE: This method runs on the main thread, but never blocks -- the blocking part of the work is // done by yielding the UnityWebRequest, which releases the main thread for other tasks while we // are waiting for the web request to complete (by the miracle of coroutines). // Let the caller create the UnityWebRequest, configuring it as they want. The caller can set the URL, // method, headers, anything they want. The only thing they can't do is call Send(), as we're in charge // of doing that. UnityWebRequest webRequest = request.creationCallback(); PtDebug.LogVerboseFormat("Web request: {0} {1}", webRequest.method, webRequest.url); bool cacheAllowed = cache != null && webRequest.method == "GET" && request.maxAgeMillis != CACHE_NONE; // Check the cache (if it's a GET request and cache is enabled). if (cacheAllowed) { bool cacheHit = false; byte[] cacheData = null; bool cacheReadDone = false; cache.RequestRead(webRequest.url, request.maxAgeMillis, (bool success, byte[] data) => { cacheHit = success; cacheData = data; cacheReadDone = true; }); while (!cacheReadDone) { yield return(null); } if (cacheHit) { PtDebug.LogVerboseFormat("Web request CACHE HIT: {0}, response: {1} bytes", webRequest.url, cacheData.Length); request.completionCallback(PolyStatus.Success(), /* responseCode */ 200, cacheData); // Return the buffer to the pool for reuse. CleanUpAfterWebRequest(bufferHolder); yield break; } else { PtDebug.LogVerboseFormat("Web request CACHE MISS: {0}.", webRequest.url); } } DownloadHandlerBuffer handler = new DownloadHandlerBuffer(); webRequest.downloadHandler = handler; // We need to asset that we actually succeeded in setting the download handler, because this can fail // if, for example, the creation callback mistakenly called Send(). PolyUtils.AssertTrue(webRequest.downloadHandler == handler, "Couldn't set download handler. It's either disposed of, or the creation callback mistakenly called Send()."); // Start the web request. This will suspend this coroutine until the request is done. PtDebug.LogVerboseFormat("Sending web request: {0}", webRequest.url); yield return(UnityCompat.SendWebRequest(webRequest)); // Request is finished. Call user-supplied callback. PtDebug.LogVerboseFormat("Web request finished: {0}, HTTP response code {1}, response: {2}", webRequest.url, webRequest.responseCode, webRequest.downloadHandler.text); PolyStatus status = UnityCompat.IsNetworkError(webRequest) ? PolyStatus.Error(webRequest.error) : PolyStatus.Success(); request.completionCallback(status, (int)webRequest.responseCode, webRequest.downloadHandler.data); // Cache the result, if applicable. if (!UnityCompat.IsNetworkError(webRequest) && cacheAllowed) { byte[] data = webRequest.downloadHandler.data; if (data != null && data.Length > 0) { byte[] copy = new byte[data.Length]; Buffer.BlockCopy(data, 0, copy, 0, data.Length); cache.RequestWrite(webRequest.url, copy); } } // Clean up. webRequest.Dispose(); CleanUpAfterWebRequest(bufferHolder); }
/// <summary> /// Executes the given import request, producing a PtAsset and a prefab. /// </summary> /// <param name="request">The request to perform.</param> private static void ExecuteImportRequest(ImportRequest request) { PtDebug.LogFormat("Executing import request: {0}", request); string gltfFullPath = PtUtils.ToAbsolutePath(request.gltfLocalPath); string assetLocalPath = request.ptAssetLocalPath; string assetFullPath = PtUtils.ToAbsolutePath(assetLocalPath); PtAsset assetToReplace = AssetDatabase.LoadAssetAtPath <PtAsset>(assetLocalPath); GameObject prefabToReplace = null; if (assetToReplace != null) { if (assetToReplace.assetPrefab == null) { Debug.LogErrorFormat("Couldn't find prefab for asset {0}.", assetToReplace); PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_FAILED, "Prefab not found"); return; } prefabToReplace = assetToReplace.assetPrefab; } // Determine if file is glTF2 or glTF1. bool isGltf2 = Path.GetExtension(request.gltfLocalPath) == ".gltf2"; // First, import the GLTF and build a GameObject from it. EditorUtility.DisplayProgressBar(PROGRESS_BAR_TITLE, PROGRESS_BAR_TEXT, 0.5f); // Use a SanitizedPath stream loader because any format file we have downloaded and saved to disk we // have replaced the original relative path string with the MD5 string hash. This custom stream loader // will always convert uris passed to it to this hash value, and read them from there. IUriLoader binLoader = new HashedPathBufferedStreamLoader(Path.GetDirectoryName(gltfFullPath)); ImportGltf.GltfImportResult result = null; using (TextReader reader = new StreamReader(gltfFullPath)) { result = ImportGltf.Import(isGltf2 ? GltfSchemaVersion.GLTF2 : GltfSchemaVersion.GLTF1, reader, binLoader, request.options.baseOptions); } EditorUtility.ClearProgressBar(); string baseName = PtUtils.GetPtAssetBaseName(request.polyAsset); result.root.name = baseName; // Create the asset (delete it first if it exists). if (File.Exists(assetFullPath)) { AssetDatabase.DeleteAsset(assetLocalPath); // If we are replacing an existing asset, we should rename the replacement to the new name, // since the name reflects the identity of the asset. So if the user is importing the asset // dog_a381b3g to replace what was previously cat_v81938.asset, the replacement file should // be named dog_a381b3g.asset, not cat_v81938.asset. assetLocalPath = PtUtils.GetDefaultPtAssetPath(request.polyAsset); assetFullPath = PtUtils.ToAbsolutePath(assetLocalPath); } Directory.CreateDirectory(Path.GetDirectoryName(assetFullPath)); // Create the new PtAsset and fill it in. AssetDatabase.CreateAsset(ScriptableObject.CreateInstance <PtAsset>(), assetLocalPath); PtAsset newAsset = AssetDatabase.LoadAssetAtPath <PtAsset>(assetLocalPath); newAsset.name = baseName; newAsset.title = request.polyAsset.displayName ?? ""; newAsset.author = request.polyAsset.authorName ?? ""; newAsset.license = request.polyAsset.license; newAsset.url = request.polyAsset.Url; // Ensure the imported object has a PtAssetObject component which references the PtAsset. result.root.AddComponent <PtAssetObject>().asset = newAsset; // Add all the meshes to the PtAsset. SaveMeshes(result.meshes, newAsset); // If the asset has materials, save those to the PtAsset. if (result.materials != null) { SaveMaterials(result.materials, newAsset); } // If the asset has textures, save those to the PtAsset. if (result.textures != null) { SaveTextures(result.textures, newAsset); } // Reimport is required to ensure custom asset displays correctly. AssetDatabase.ImportAsset(assetLocalPath); GameObject newPrefab; if (prefabToReplace) { // Replace the existing prefab with our new object, without breaking prefab connections. newPrefab = PrefabUtility.ReplacePrefab(result.root, prefabToReplace, ReplacePrefabOptions.ReplaceNameBased); AssetDatabase.RenameAsset(AssetDatabase.GetAssetPath(newPrefab), baseName); } else { // Create a new prefab. // Prefab path is the same as the asset path but with the extension changed to '.prefab'. string prefabLocalPath = Regex.Replace(assetLocalPath, "\\.asset$", ".prefab"); if (!prefabLocalPath.EndsWith(".prefab")) { Debug.LogErrorFormat("Error: failed to compute prefab path for {0}", assetLocalPath); PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_FAILED, "Prefab path error"); return; } newPrefab = PrefabUtility.CreatePrefab(prefabLocalPath, result.root); } // Now ensure the asset points to the prefab. newAsset.assetPrefab = newPrefab; if (newAsset.assetPrefab == null) { Debug.LogErrorFormat("Could not get asset prefab reference for asset {0}", newAsset); PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_FAILED, "Prefab ref error"); } GameObject.DestroyImmediate(result.root); AssetDatabase.Refresh(); if (request.options.alsoInstantiate) { PrefabUtility.InstantiatePrefab(newPrefab); } PtDebug.LogFormat("GLTF import complete: {0}", request); PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_SUCCESSFUL, isGltf2 ? "GLTF2" : "GLTF1"); // If this is a third-party asset, we need to update the attributions file. AttributionFileGenerator.Generate(/* showUi */ false); EditorWindow.GetWindow <AssetBrowserWindow>().HandleAssetImported(request.polyAsset.name); // Select the prefab in the editor so the user knows where it is. AssetDatabase.Refresh(); Selection.activeObject = newPrefab; EditorGUIUtility.PingObject(newPrefab); }