summaryrefslogtreecommitdiff
path: root/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts
diff options
context:
space:
mode:
Diffstat (limited to 'VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts')
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs1903
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs20
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs20
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs133
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs49
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback.meta8
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs42
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs324
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs421
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs681
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs18
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs.meta12
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation.meta8
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs1
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance.meta8
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs165
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs.meta12
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs37
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters.meta8
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs109
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs33
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs46
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs12
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs12
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs44
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners.meta8
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs97
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs44
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs29
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs48
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs191
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs29
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs31
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs266
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs114
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs64
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs31
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats.meta8
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs757
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs32
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs15
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs.meta11
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs86
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs.meta3
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs260
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs.meta12
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs682
-rw-r--r--VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs.meta12
78 files changed, 7126 insertions, 0 deletions
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs
new file mode 100644
index 00000000..0dedf888
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs
@@ -0,0 +1,1903 @@
+#if UNITY_EDITOR
+using UnityEngine;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System;
+using System.IO;
+using Debug = UnityEngine.Debug;
+using System.Text.RegularExpressions;
+
+namespace VRC.Core
+{
+ public class ApiFileHelper : MonoBehaviour
+ {
+ private readonly int kMultipartUploadChunkSize = 100 * 1024 * 1024; // 100 MB
+ private readonly int SERVER_PROCESSING_WAIT_TIMEOUT_CHUNK_SIZE = 50 * 1024 * 1024;
+ private readonly float SERVER_PROCESSING_WAIT_TIMEOUT_PER_CHUNK_SIZE = 120.0f;
+ private readonly float SERVER_PROCESSING_MAX_WAIT_TIMEOUT = 600.0f;
+ private readonly float SERVER_PROCESSING_INITIAL_RETRY_TIME = 2.0f;
+ private readonly float SERVER_PROCESSING_MAX_RETRY_TIME = 10.0f;
+
+ private static bool EnableDeltaCompression = false;
+
+ private readonly Regex[] kUnityPackageAssetNameFilters = new Regex[]
+ {
+ new Regex(@"/LightingData\.asset$"), // lightmap base asset
+ new Regex(@"/Lightmap-.*(\.png|\.exr)$"), // lightmaps
+ new Regex(@"/ReflectionProbe-.*(\.exr|\.png)$"), // reflection probes
+ new Regex(@"/Editor/Data/UnityExtensions/") // anything that looks like part of the Unity installation
+ };
+
+ public delegate void OnFileOpSuccess(ApiFile apiFile, string message);
+ public delegate void OnFileOpError(ApiFile apiFile, string error);
+ public delegate void OnFileOpProgress(ApiFile apiFile, string status, string subStatus, float pct);
+ public delegate bool FileOpCancelQuery(ApiFile apiFile);
+
+ public static ApiFileHelper Instance
+ {
+ get
+ {
+ CheckInstance();
+ return mInstance;
+ }
+ }
+
+ private static ApiFileHelper mInstance = null;
+ const float kPostWriteDelay = 0.75f;
+
+ public enum FileOpResult
+ {
+ Success,
+ Unchanged
+ }
+
+ public static string GetMimeTypeFromExtension(string extension)
+ {
+ if (extension == ".vrcw")
+ return "application/x-world";
+ if (extension == ".vrca")
+ return "application/x-avatar";
+ if (extension == ".dll")
+ return "application/x-msdownload";
+ if (extension == ".unitypackage")
+ return "application/gzip";
+ if (extension == ".gz")
+ return "application/gzip";
+ if (extension == ".jpg")
+ return "image/jpg";
+ if (extension == ".png")
+ return "image/png";
+ if (extension == ".sig")
+ return "application/x-rsync-signature";
+ if (extension == ".delta")
+ return "application/x-rsync-delta";
+
+ Debug.LogWarning("Unknown file extension for mime-type: " + extension);
+ return "application/octet-stream";
+ }
+
+ public static bool IsGZipCompressed(string filename)
+ {
+ return GetMimeTypeFromExtension(Path.GetExtension(filename)) == "application/gzip";
+ }
+
+ public IEnumerator UploadFile(string filename, string existingFileId, string friendlyName,
+ OnFileOpSuccess onSuccess, OnFileOpError onError, OnFileOpProgress onProgress, FileOpCancelQuery cancelQuery)
+ {
+ VRC.Core.Logger.Log("UploadFile: filename: " + filename + ", file id: " +
+ (!string.IsNullOrEmpty(existingFileId) ? existingFileId : "<new>") + ", name: " + friendlyName, DebugLevel.All);
+
+ // init remote config
+ if (!ConfigManager.RemoteConfig.IsInitialized())
+ {
+ bool done = false;
+ ConfigManager.RemoteConfig.Init(
+ delegate () { done = true; },
+ delegate () { done = true; }
+ );
+
+ while (!done)
+ yield return null;
+
+ if (!ConfigManager.RemoteConfig.IsInitialized())
+ {
+ Error(onError, null, "Failed to fetch configuration.");
+ yield break;
+ }
+ }
+
+ // configure delta compression
+ {
+ EnableDeltaCompression = ConfigManager.RemoteConfig.GetBool("sdkEnableDeltaCompression", false);
+ }
+
+ // validate input file
+ Progress(onProgress, null, "Checking file...");
+
+ if (string.IsNullOrEmpty(filename))
+ {
+ Error(onError, null, "Upload filename is empty!");
+ yield break;
+ }
+
+ if (!System.IO.Path.HasExtension(filename))
+ {
+ Error(onError, null, "Upload filename must have an extension: " + filename);
+ yield break;
+ }
+
+ string whyNot;
+ if (!VRC.Tools.FileCanRead(filename, out whyNot))
+ {
+ Error(onError, null, "Could not read file to upload!", filename + "\n" + whyNot);
+ yield break;
+ }
+
+ // get or create ApiFile
+ Progress(onProgress, null, string.IsNullOrEmpty(existingFileId) ? "Creating file record..." : "Getting file record...");
+
+ bool wait = true;
+ bool wasError = false;
+ bool worthRetry = false;
+ string errorStr = "";
+
+ if (string.IsNullOrEmpty(friendlyName))
+ friendlyName = filename;
+
+ string extension = System.IO.Path.GetExtension(filename);
+ string mimeType = GetMimeTypeFromExtension(extension);
+
+ ApiFile apiFile = null;
+
+ System.Action<ApiContainer> fileSuccess = (ApiContainer c) =>
+ {
+ apiFile = c.Model as ApiFile;
+ wait = false;
+ };
+
+ System.Action<ApiContainer> fileFailure = (ApiContainer c) =>
+ {
+ errorStr = c.Error;
+ wait = false;
+
+ if (c.Code == 400)
+ worthRetry = true;
+ };
+
+ while (true)
+ {
+ apiFile = null;
+ wait = true;
+ worthRetry = false;
+ errorStr = "";
+
+ if (string.IsNullOrEmpty(existingFileId))
+ ApiFile.Create(friendlyName, mimeType, extension, fileSuccess, fileFailure);
+ else
+ API.Fetch<ApiFile>(existingFileId, fileSuccess, fileFailure);
+
+ while (wait)
+ {
+ if (apiFile != null && CheckCancelled(cancelQuery, onError, apiFile))
+ yield break;
+
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ if (errorStr.Contains("File not found"))
+ {
+ Debug.LogError("Couldn't find file record: " + existingFileId + ", creating new file record");
+
+ existingFileId = "";
+ continue;
+ }
+
+ string msg = string.IsNullOrEmpty(existingFileId) ? "Failed to create file record." : "Failed to get file record.";
+ Error(onError, null, msg, errorStr);
+
+ if (!worthRetry)
+ yield break;
+ }
+
+ if (!worthRetry)
+ break;
+ else
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+ }
+
+ if (apiFile == null)
+ yield break;
+
+ LogApiFileStatus(apiFile, false, true);
+
+ while (apiFile.HasQueuedOperation(EnableDeltaCompression))
+ {
+ wait = true;
+
+ apiFile.DeleteLatestVersion((c) => wait = false, (c) => wait = false);
+
+ while (wait)
+ {
+ if (apiFile != null && CheckCancelled(cancelQuery, onError, apiFile))
+ yield break;
+
+ yield return null;
+ }
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ LogApiFileStatus(apiFile, false);
+
+ // check for server side errors from last upload
+ if (apiFile.IsInErrorState())
+ {
+ Debug.LogWarning("ApiFile: " + apiFile.id + ": server failed to process last uploaded, deleting failed version");
+
+ while (true)
+ {
+ // delete previous failed version
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Cleaning up previous version");
+
+ wait = true;
+ errorStr = "";
+ worthRetry = false;
+
+ apiFile.DeleteLatestVersion(fileSuccess, fileFailure);
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, null))
+ {
+ yield break;
+ }
+
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to delete previous failed version!", errorStr);
+ if (!worthRetry)
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ if (worthRetry)
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+ else
+ break;
+ }
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ LogApiFileStatus(apiFile, false);
+
+ // verify previous file op is complete
+ if (apiFile.HasQueuedOperation(EnableDeltaCompression))
+ {
+ Error(onError, apiFile, "A previous upload is still being processed. Please try again later.");
+ yield break;
+ }
+
+ if (wasError)
+ yield break;
+
+ LogApiFileStatus(apiFile, false);
+
+ // generate md5 and check if file has changed
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Generating file hash");
+
+ string fileMD5Base64 = "";
+ wait = true;
+ errorStr = "";
+ VRC.Tools.FileMD5(filename,
+ delegate (byte[] md5Bytes)
+ {
+ fileMD5Base64 = Convert.ToBase64String(md5Bytes);
+ wait = false;
+ },
+ delegate (string error)
+ {
+ errorStr = filename + "\n" + error;
+ wait = false;
+ }
+ );
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to generate MD5 hash for upload file.", errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ LogApiFileStatus(apiFile, false);
+
+ // check if file has been changed
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Checking for changes");
+
+ bool isPreviousUploadRetry = false;
+ if (apiFile.HasExistingOrPendingVersion())
+ {
+ // uploading the same file?
+ if (string.Compare(fileMD5Base64, apiFile.GetFileMD5(apiFile.GetLatestVersionNumber())) == 0)
+ {
+ // the previous operation completed successfully?
+ if (!apiFile.IsWaitingForUpload())
+ {
+ Success(onSuccess, apiFile, "The file to upload is unchanged.");
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ else
+ {
+ isPreviousUploadRetry = true;
+
+ Debug.Log("Retrying previous upload");
+ }
+ }
+ else
+ {
+ // the file has been modified
+ if (apiFile.IsWaitingForUpload())
+ {
+ // previous upload failed, and the file is changed
+ while (true)
+ {
+ // delete previous failed version
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Cleaning up previous version");
+
+ wait = true;
+ worthRetry = false;
+ errorStr = "";
+
+ apiFile.DeleteLatestVersion(fileSuccess, fileFailure);
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to delete previous incomplete version!", errorStr);
+ if (!worthRetry)
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+ }
+ }
+ }
+
+ LogApiFileStatus(apiFile, false);
+
+ // generate signature for new file
+
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Generating signature");
+
+ string signatureFilename = VRC.Tools.GetTempFileName(".sig", out errorStr, apiFile.id);
+ if (string.IsNullOrEmpty(signatureFilename))
+ {
+ Error(onError, apiFile, "Failed to generate file signature!", "Failed to create temp file: \n" + errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ wasError = false;
+ yield return StartCoroutine(CreateFileSignatureInternal(filename, signatureFilename,
+ delegate ()
+ {
+ // success!
+ },
+ delegate (string error)
+ {
+ Error(onError, apiFile, "Failed to generate file signature!", error);
+ CleanupTempFiles(apiFile.id);
+ wasError = true;
+ })
+ );
+
+ if (wasError)
+ yield break;
+
+ LogApiFileStatus(apiFile, false);
+
+ // generate signature md5 and file size
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Generating signature hash");
+
+ string sigMD5Base64 = "";
+ wait = true;
+ errorStr = "";
+ VRC.Tools.FileMD5(signatureFilename,
+ delegate (byte[] md5Bytes)
+ {
+ sigMD5Base64 = Convert.ToBase64String(md5Bytes);
+ wait = false;
+ },
+ delegate (string error)
+ {
+ errorStr = signatureFilename + "\n" + error;
+ wait = false;
+ }
+ );
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to generate MD5 hash for signature file.", errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ long sigFileSize = 0;
+ if (!VRC.Tools.GetFileSize(signatureFilename, out sigFileSize, out errorStr))
+ {
+ Error(onError, apiFile, "Failed to generate file signature!", "Couldn't get file size:\n" + errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ LogApiFileStatus(apiFile, false);
+
+ // download previous version signature (if exists)
+ string existingFileSignaturePath = null;
+ if (EnableDeltaCompression && apiFile.HasExistingVersion())
+ {
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Downloading previous version signature");
+
+ wait = true;
+ errorStr = "";
+ apiFile.DownloadSignature(
+ delegate (byte[] data)
+ {
+ // save to temp file
+ existingFileSignaturePath = VRC.Tools.GetTempFileName(".sig", out errorStr, apiFile.id);
+ if (string.IsNullOrEmpty(existingFileSignaturePath))
+ {
+ errorStr = "Failed to create temp file: \n" + errorStr;
+ wait = false;
+ }
+ else
+ {
+ try
+ {
+ File.WriteAllBytes(existingFileSignaturePath, data);
+ }
+ catch (Exception e)
+ {
+ existingFileSignaturePath = null;
+ errorStr = "Failed to write signature temp file:\n" + e.Message;
+ }
+ wait = false;
+ }
+ },
+ delegate (string error)
+ {
+ errorStr = error;
+ wait = false;
+ },
+ delegate (long downloaded, long length)
+ {
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Downloading previous version signature", Tools.DivideSafe(downloaded, length));
+ }
+ );
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to download previous file version signature.", errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ LogApiFileStatus(apiFile, false);
+
+ // create delta if needed
+ string deltaFilename = null;
+
+ if (EnableDeltaCompression && !string.IsNullOrEmpty(existingFileSignaturePath))
+ {
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Creating file delta");
+
+ deltaFilename = VRC.Tools.GetTempFileName(".delta", out errorStr, apiFile.id);
+ if (string.IsNullOrEmpty(deltaFilename))
+ {
+ Error(onError, apiFile, "Failed to create file delta for upload.", "Failed to create temp file: \n" + errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ wasError = false;
+ yield return StartCoroutine(CreateFileDeltaInternal(filename, existingFileSignaturePath, deltaFilename,
+ delegate ()
+ {
+ // success!
+ },
+ delegate (string error)
+ {
+ Error(onError, apiFile, "Failed to create file delta for upload.", error);
+ CleanupTempFiles(apiFile.id);
+ wasError = true;
+ })
+ );
+
+ if (wasError)
+ yield break;
+ }
+
+ // upload smaller of delta and new file
+ long fullFileSize = 0;
+ long deltaFileSize = 0;
+ if (!VRC.Tools.GetFileSize(filename, out fullFileSize, out errorStr) ||
+ (!string.IsNullOrEmpty(deltaFilename) && !VRC.Tools.GetFileSize(deltaFilename, out deltaFileSize, out errorStr)))
+ {
+ Error(onError, apiFile, "Failed to create file delta for upload.", "Couldn't get file size: " + errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ bool uploadDeltaFile = EnableDeltaCompression && deltaFileSize > 0 && deltaFileSize < fullFileSize;
+ if (EnableDeltaCompression)
+ VRC.Core.Logger.Log("Delta size " + deltaFileSize + " (" + ((float)deltaFileSize / (float)fullFileSize) + " %), full file size " + fullFileSize + ", uploading " + (uploadDeltaFile ? " DELTA" : " FULL FILE"), DebugLevel.All);
+ else
+ VRC.Core.Logger.Log("Delta compression disabled, uploading FULL FILE, size " + fullFileSize, DebugLevel.All);
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ string deltaMD5Base64 = "";
+ if (uploadDeltaFile)
+ {
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Generating file delta hash");
+
+ wait = true;
+ errorStr = "";
+ VRC.Tools.FileMD5(deltaFilename,
+ delegate (byte[] md5Bytes)
+ {
+ deltaMD5Base64 = Convert.ToBase64String(md5Bytes);
+ wait = false;
+ },
+ delegate (string error)
+ {
+ errorStr = error;
+ wait = false;
+ }
+ );
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to generate file delta hash.", errorStr);
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ // validate existing pending version info, if this is a retry
+ bool versionAlreadyExists = false;
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ if (isPreviousUploadRetry)
+ {
+ bool isValid = true;
+
+ ApiFile.Version v = apiFile.GetVersion(apiFile.GetLatestVersionNumber());
+ if (v != null)
+ {
+ if (uploadDeltaFile)
+ {
+ isValid = deltaFileSize == v.delta.sizeInBytes &&
+ deltaMD5Base64.CompareTo(v.delta.md5) == 0 &&
+ sigFileSize == v.signature.sizeInBytes &&
+ sigMD5Base64.CompareTo(v.signature.md5) == 0;
+ }
+ else
+ {
+ isValid = fullFileSize == v.file.sizeInBytes &&
+ fileMD5Base64.CompareTo(v.file.md5) == 0 &&
+ sigFileSize == v.signature.sizeInBytes &&
+ sigMD5Base64.CompareTo(v.signature.md5) == 0;
+ }
+ }
+ else
+ {
+ isValid = false;
+ }
+
+ if (isValid)
+ {
+ versionAlreadyExists = true;
+
+ Debug.Log("Using existing version record");
+ }
+ else
+ {
+ // delete previous invalid version
+ Progress(onProgress, apiFile, "Preparing file for upload...", "Cleaning up previous version");
+
+ while (true)
+ {
+ wait = true;
+ errorStr = "";
+ worthRetry = false;
+
+ apiFile.DeleteLatestVersion(fileSuccess, fileFailure);
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, null))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to delete previous incomplete version!", errorStr);
+ if (!worthRetry)
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+ }
+ }
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ // create new version of file
+ if (!versionAlreadyExists)
+ {
+ while (true)
+ {
+ Progress(onProgress, apiFile, "Creating file version record...");
+
+ wait = true;
+ errorStr = "";
+ worthRetry = false;
+
+ if (uploadDeltaFile)
+ // delta file
+ apiFile.CreateNewVersion(ApiFile.Version.FileType.Delta, deltaMD5Base64, deltaFileSize, sigMD5Base64, sigFileSize, fileSuccess, fileFailure);
+ else
+ // full file
+ apiFile.CreateNewVersion(ApiFile.Version.FileType.Full, fileMD5Base64, fullFileSize, sigMD5Base64, sigFileSize, fileSuccess, fileFailure);
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Failed to create file version record.", errorStr);
+ if (!worthRetry)
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+ }
+
+ // upload components
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ // upload delta
+ if (uploadDeltaFile)
+ {
+ if (apiFile.GetLatestVersion().delta.status == ApiFile.Status.Waiting)
+ {
+ Progress(onProgress, apiFile, "Uploading file delta...");
+
+ wasError = false;
+ yield return StartCoroutine(UploadFileComponentInternal(apiFile,
+ ApiFile.Version.FileDescriptor.Type.delta, deltaFilename, deltaMD5Base64, deltaFileSize,
+ delegate (ApiFile file)
+ {
+ Debug.Log("Successfully uploaded file delta.");
+ apiFile = file;
+ },
+ delegate (string error)
+ {
+ Error(onError, apiFile, "Failed to upload file delta.", error);
+ CleanupTempFiles(apiFile.id);
+ wasError = true;
+ },
+ delegate (long downloaded, long length)
+ {
+ Progress(onProgress, apiFile, "Uploading file delta...", "", Tools.DivideSafe(downloaded, length));
+ },
+ cancelQuery)
+ );
+
+ if (wasError)
+ yield break;
+ }
+ }
+ // upload file
+ else
+ {
+ if (apiFile.GetLatestVersion().file.status == ApiFile.Status.Waiting)
+ {
+ Progress(onProgress, apiFile, "Uploading file...");
+
+ wasError = false;
+ yield return StartCoroutine(UploadFileComponentInternal(apiFile,
+ ApiFile.Version.FileDescriptor.Type.file, filename, fileMD5Base64, fullFileSize,
+ delegate (ApiFile file)
+ {
+ VRC.Core.Logger.Log("Successfully uploaded file.", DebugLevel.All);
+ apiFile = file;
+ },
+ delegate (string error)
+ {
+ Error(onError, apiFile, "Failed to upload file.", error);
+ CleanupTempFiles(apiFile.id);
+ wasError = true;
+ },
+ delegate (long downloaded, long length)
+ {
+ Progress(onProgress, apiFile, "Uploading file...", "", Tools.DivideSafe(downloaded, length));
+ },
+ cancelQuery)
+ );
+
+ if (wasError)
+ yield break;
+ }
+ }
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ // upload signature
+ if (apiFile.GetLatestVersion().signature.status == ApiFile.Status.Waiting)
+ {
+ Progress(onProgress, apiFile, "Uploading file signature...");
+
+ wasError = false;
+ yield return StartCoroutine(UploadFileComponentInternal(apiFile,
+ ApiFile.Version.FileDescriptor.Type.signature, signatureFilename, sigMD5Base64, sigFileSize,
+ delegate (ApiFile file)
+ {
+ VRC.Core.Logger.Log("Successfully uploaded file signature.", DebugLevel.All);
+ apiFile = file;
+ },
+ delegate (string error)
+ {
+ Error(onError, apiFile, "Failed to upload file signature.", error);
+ CleanupTempFiles(apiFile.id);
+ wasError = true;
+ },
+ delegate (long downloaded, long length)
+ {
+ Progress(onProgress, apiFile, "Uploading file signature...", "", Tools.DivideSafe(downloaded, length));
+ },
+ cancelQuery)
+ );
+
+ if (wasError)
+ yield break;
+ }
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ // Validate file records queued or complete
+ Progress(onProgress, apiFile, "Validating upload...");
+
+ bool isUploadComplete = (uploadDeltaFile
+ ? apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.delta).status == ApiFile.Status.Complete
+ : apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.file).status == ApiFile.Status.Complete);
+ isUploadComplete = isUploadComplete &&
+ apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.signature).status == ApiFile.Status.Complete;
+
+ if (!isUploadComplete)
+ {
+ Error(onError, apiFile, "Failed to upload file.", "Record status is not 'complete'");
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ bool isServerOpQueuedOrComplete = (uploadDeltaFile
+ ? apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.file).status != ApiFile.Status.Waiting
+ : apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), ApiFile.Version.FileDescriptor.Type.delta).status != ApiFile.Status.Waiting);
+
+ if (!isServerOpQueuedOrComplete)
+ {
+ Error(onError, apiFile, "Failed to upload file.", "Record is still in 'waiting' status");
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ // wait for server processing to complete
+ Progress(onProgress, apiFile, "Processing upload...");
+ float checkDelay = SERVER_PROCESSING_INITIAL_RETRY_TIME;
+ float maxDelay = SERVER_PROCESSING_MAX_RETRY_TIME;
+ float timeout = GetServerProcessingWaitTimeoutForDataSize(apiFile.GetLatestVersion().file.sizeInBytes);
+ double initialStartTime = Time.realtimeSinceStartup;
+ double startTime = initialStartTime;
+ while (apiFile.HasQueuedOperation(uploadDeltaFile))
+ {
+ // wait before polling again
+ Progress(onProgress, apiFile, "Processing upload...", "Checking status in " + Mathf.CeilToInt(checkDelay) + " seconds");
+
+ while (Time.realtimeSinceStartup - startTime < checkDelay)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ if (Time.realtimeSinceStartup - initialStartTime > timeout)
+ {
+ LogApiFileStatus(apiFile, uploadDeltaFile);
+
+ Error(onError, apiFile, "Timed out waiting for upload processing to complete.");
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ yield return null;
+ }
+
+ while (true)
+ {
+ // check status
+ Progress(onProgress, apiFile, "Processing upload...", "Checking status...");
+
+ wait = true;
+ worthRetry = false;
+ errorStr = "";
+ API.Fetch<ApiFile>(apiFile.id, fileSuccess, fileFailure);
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onError, apiFile))
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ Error(onError, apiFile, "Checking upload status failed.", errorStr);
+ if (!worthRetry)
+ {
+ CleanupTempFiles(apiFile.id);
+ yield break;
+ }
+ }
+
+ if (!worthRetry)
+ break;
+ }
+
+ checkDelay = Mathf.Min(checkDelay * 2, maxDelay);
+ startTime = Time.realtimeSinceStartup;
+ }
+
+ // cleanup and wait for it to finish
+ yield return StartCoroutine(CleanupTempFilesInternal(apiFile.id));
+
+ Success(onSuccess, apiFile, "Upload complete!");
+ }
+
+ private static void LogApiFileStatus(ApiFile apiFile, bool checkDelta, bool logSuccess = false)
+ {
+ if (apiFile == null || !apiFile.IsInitialized)
+ {
+ Debug.LogFormat("<color=yellow>apiFile not initialized</color>");
+ }
+ else if (apiFile.IsInErrorState())
+ {
+ Debug.LogFormat("<color=yellow>ApiFile {0} is in an error state.</color>", apiFile.name);
+ }
+ else if (logSuccess)
+ VRC.Core.Logger.Log("< color = yellow > Processing { 3}: { 0}, { 1}, { 2}</ color > " +
+ (apiFile.IsWaitingForUpload() ? "waiting for upload" : "upload complete") +
+ (apiFile.HasExistingOrPendingVersion() ? "has existing or pending version" : "no previous version") +
+ (apiFile.IsLatestVersionQueued(checkDelta) ? "latest version queued" : "latest version not queued") +
+ apiFile.name, DebugLevel.All);
+
+ if (apiFile != null && apiFile.IsInitialized && logSuccess)
+ {
+ var apiFields = apiFile.ExtractApiFields();
+ if (apiFields != null)
+ VRC.Core.Logger.Log("<color=yellow>{0}</color>" + VRC.Tools.JsonEncode(apiFields), DebugLevel.All);
+ }
+ }
+
+ public IEnumerator CreateFileSignatureInternal(string filename, string outputSignatureFilename, Action onSuccess, Action<string> onError)
+ {
+ VRC.Core.Logger.Log("CreateFileSignature: " + filename + " => " + outputSignatureFilename, DebugLevel.All);
+
+ yield return null;
+
+ Stream inStream = null;
+ FileStream outStream = null;
+ byte[] buf = new byte[64 * 1024];
+ IAsyncResult asyncRead = null;
+ IAsyncResult asyncWrite = null;
+
+ try
+ {
+ inStream = librsync.net.Librsync.ComputeSignature(File.OpenRead(filename));
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't open input file: " + e.Message);
+ yield break;
+ }
+
+ try
+ {
+ outStream = File.Open(outputSignatureFilename, FileMode.Create, FileAccess.Write);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't create output file: " + e.Message);
+ yield break;
+ }
+
+ while (true)
+ {
+ try
+ {
+ asyncRead = inStream.BeginRead(buf, 0, buf.Length, null, null);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't read file: " + e.Message);
+ yield break;
+ }
+
+ while (!asyncRead.IsCompleted)
+ yield return null;
+
+ int read = 0;
+ try
+ {
+ read = inStream.EndRead(asyncRead);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't read file: " + e.Message);
+ yield break;
+ }
+
+ if (read <= 0)
+ break;
+
+ try
+ {
+ asyncWrite = outStream.BeginWrite(buf, 0, read, null, null);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't write file: " + e.Message);
+ yield break;
+ }
+
+ while (!asyncWrite.IsCompleted)
+ yield return null;
+
+ try
+ {
+ outStream.EndWrite(asyncWrite);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't write file: " + e.Message);
+ yield break;
+ }
+ }
+
+ inStream.Close();
+ outStream.Close();
+
+ yield return null;
+
+ if (onSuccess != null)
+ onSuccess();
+ }
+
+ public IEnumerator CreateFileDeltaInternal(string newFilename, string existingFileSignaturePath, string outputDeltaFilename, Action onSuccess, Action<string> onError)
+ {
+ Debug.Log("CreateFileDelta: " + newFilename + " (delta) " + existingFileSignaturePath + " => " + outputDeltaFilename);
+
+ yield return null;
+
+ Stream inStream = null;
+ FileStream outStream = null;
+ byte[] buf = new byte[64 * 1024];
+ IAsyncResult asyncRead = null;
+ IAsyncResult asyncWrite = null;
+
+ try
+ {
+ inStream = librsync.net.Librsync.ComputeDelta(File.OpenRead(existingFileSignaturePath), File.OpenRead(newFilename));
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't open input file: " + e.Message);
+ yield break;
+ }
+
+ try
+ {
+ outStream = File.Open(outputDeltaFilename, FileMode.Create, FileAccess.Write);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't create output file: " + e.Message);
+ yield break;
+ }
+
+ while (true)
+ {
+ try
+ {
+ asyncRead = inStream.BeginRead(buf, 0, buf.Length, null, null);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't read file: " + e.Message);
+ yield break;
+ }
+
+ while (!asyncRead.IsCompleted)
+ yield return null;
+
+ int read = 0;
+ try
+ {
+ read = inStream.EndRead(asyncRead);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't read file: " + e.Message);
+ yield break;
+ }
+
+ if (read <= 0)
+ break;
+
+ try
+ {
+ asyncWrite = outStream.BeginWrite(buf, 0, read, null, null);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't write file: " + e.Message);
+ yield break;
+ }
+
+ while (!asyncWrite.IsCompleted)
+ yield return null;
+
+ try
+ {
+ outStream.EndWrite(asyncWrite);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't write file: " + e.Message);
+ yield break;
+ }
+ }
+
+ inStream.Close();
+ outStream.Close();
+
+ yield return null;
+
+ if (onSuccess != null)
+ onSuccess();
+ }
+
+ protected static void Success(OnFileOpSuccess onSuccess, ApiFile apiFile, string message)
+ {
+ if (apiFile == null)
+ apiFile = new ApiFile();
+
+ VRC.Core.Logger.Log("ApiFile " + apiFile.ToStringBrief() + ": Operation Succeeded!", DebugLevel.All);
+ if (onSuccess != null)
+ onSuccess(apiFile, message);
+ }
+
+ protected static void Error(OnFileOpError onError, ApiFile apiFile, string error, string moreInfo = "")
+ {
+ if (apiFile == null)
+ apiFile = new ApiFile();
+
+ Debug.LogError("ApiFile " + apiFile.ToStringBrief() + ": Error: " + error + "\n" + moreInfo);
+ if (onError != null)
+ onError(apiFile, error);
+ }
+
+ protected static void Progress(OnFileOpProgress onProgress, ApiFile apiFile, string status, string subStatus = "", float pct = 0.0f)
+ {
+ if (apiFile == null)
+ apiFile = new ApiFile();
+
+ if (onProgress != null)
+ onProgress(apiFile, status, subStatus, pct);
+ }
+
+ protected static bool CheckCancelled(FileOpCancelQuery cancelQuery, OnFileOpError onError, ApiFile apiFile)
+ {
+ if (apiFile == null)
+ {
+ Debug.LogError("apiFile was null");
+ return true;
+ }
+
+ if (cancelQuery != null && cancelQuery(apiFile))
+ {
+ Debug.Log("ApiFile " + apiFile.ToStringBrief() + ": Operation cancelled");
+ if (onError != null)
+ onError(apiFile, "Cancelled by user.");
+ return true;
+ }
+
+ return false;
+ }
+
+ protected static void CleanupTempFiles(string subFolderName)
+ {
+ Instance.StartCoroutine(Instance.CleanupTempFilesInternal(subFolderName));
+ }
+
+ protected IEnumerator CleanupTempFilesInternal(string subFolderName)
+ {
+ if (!string.IsNullOrEmpty(subFolderName))
+ {
+ string folder = VRC.Tools.GetTempFolderPath(subFolderName);
+
+ while (Directory.Exists(folder))
+ {
+ try
+ {
+ if (Directory.Exists(folder))
+ Directory.Delete(folder, true);
+ }
+ catch (System.Exception)
+ {
+ }
+
+ yield return null;
+ }
+ }
+ }
+
+ private static void CheckInstance()
+ {
+ if (mInstance == null)
+ {
+ GameObject go = new GameObject("ApiFileHelper");
+ mInstance = go.AddComponent<ApiFileHelper>();
+
+ try
+ {
+ GameObject.DontDestroyOnLoad(go);
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ private float GetServerProcessingWaitTimeoutForDataSize(int size)
+ {
+ float timeoutMultiplier = Mathf.Ceil((float)size / (float)SERVER_PROCESSING_WAIT_TIMEOUT_CHUNK_SIZE);
+ return Mathf.Clamp(timeoutMultiplier * SERVER_PROCESSING_WAIT_TIMEOUT_PER_CHUNK_SIZE, SERVER_PROCESSING_WAIT_TIMEOUT_PER_CHUNK_SIZE, SERVER_PROCESSING_MAX_WAIT_TIMEOUT);
+ }
+
+ private bool UploadFileComponentValidateFileDesc(ApiFile apiFile, string filename, string md5Base64, long fileSize, ApiFile.Version.FileDescriptor fileDesc, Action<ApiFile> onSuccess, Action<string> onError)
+ {
+ if (fileDesc.status != ApiFile.Status.Waiting)
+ {
+ // nothing to do (might be a retry)
+ Debug.Log("UploadFileComponent: (file record not in waiting status, done)");
+ if (onSuccess != null)
+ onSuccess(apiFile);
+ return false;
+ }
+
+ if (fileSize != fileDesc.sizeInBytes)
+ {
+ if (onError != null)
+ onError("File size does not match version descriptor");
+ return false;
+ }
+ if (string.Compare(md5Base64, fileDesc.md5) != 0)
+ {
+ if (onError != null)
+ onError("File MD5 does not match version descriptor");
+ return false;
+ }
+
+ // make sure file is right size
+ long tempSize = 0;
+ string errorStr = "";
+ if (!VRC.Tools.GetFileSize(filename, out tempSize, out errorStr))
+ {
+ if (onError != null)
+ onError("Couldn't get file size");
+ return false;
+ }
+ if (tempSize != fileSize)
+ {
+ if (onError != null)
+ onError("File size does not match input size");
+ return false;
+ }
+
+ return true;
+ }
+
+ private IEnumerator UploadFileComponentDoSimpleUpload(ApiFile apiFile,
+ ApiFile.Version.FileDescriptor.Type fileDescriptorType,
+ string filename,
+ string md5Base64,
+ long fileSize,
+ Action<ApiFile> onSuccess,
+ Action<string> onError,
+ Action<long, long> onProgress,
+ FileOpCancelQuery cancelQuery)
+ {
+ OnFileOpError onCancelFunc = delegate (ApiFile file, string s)
+ {
+ if (onError != null)
+ onError(s);
+ };
+
+ string uploadUrl = "";
+ while (true)
+ {
+ bool wait = true;
+ string errorStr = "";
+ bool worthRetry = false;
+
+ apiFile.StartSimpleUpload(fileDescriptorType,
+ (c) =>
+ {
+ uploadUrl = (c as ApiDictContainer).ResponseDictionary["url"] as string;
+ wait = false;
+ },
+ (c) =>
+ {
+ errorStr = "Failed to start upload: " + c.Error;
+ wait = false;
+ if (c.Code == 400)
+ worthRetry = true;
+ });
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ if (onError != null)
+ onError(errorStr);
+ if (!worthRetry)
+ yield break;
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+
+ // PUT file
+ {
+ bool wait = true;
+ string errorStr = "";
+
+ VRC.HttpRequest req = ApiFile.PutSimpleFileToURL(uploadUrl, filename, GetMimeTypeFromExtension(Path.GetExtension(filename)), md5Base64, true,
+ delegate ()
+ {
+ wait = false;
+ },
+ delegate (string error)
+ {
+ errorStr = "Failed to upload file: " + error;
+ wait = false;
+ },
+ delegate (long uploaded, long length)
+ {
+ if (onProgress != null)
+ onProgress(uploaded, length);
+ }
+ );
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ if (req != null)
+ req.Abort();
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ if (onError != null)
+ onError(errorStr);
+ yield break;
+ }
+ }
+
+ // finish upload
+ while (true)
+ {
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ bool wait = true;
+ string errorStr = "";
+ bool worthRetry = false;
+
+ apiFile.FinishUpload(fileDescriptorType, null,
+ (c) =>
+ {
+ apiFile = c.Model as ApiFile;
+ wait = false;
+ },
+ (c) =>
+ {
+ errorStr = "Failed to finish upload: " + c.Error;
+ wait = false;
+ if (c.Code == 400)
+ worthRetry = false;
+ });
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ if (onError != null)
+ onError(errorStr);
+ if (!worthRetry)
+ yield break;
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+
+ }
+
+ private IEnumerator UploadFileComponentDoMultipartUpload(ApiFile apiFile,
+ ApiFile.Version.FileDescriptor.Type fileDescriptorType,
+ string filename,
+ string md5Base64,
+ long fileSize,
+ Action<ApiFile> onSuccess,
+ Action<string> onError,
+ Action<long, long> onProgress,
+ FileOpCancelQuery cancelQuery)
+ {
+ FileStream fs = null;
+ OnFileOpError onCancelFunc = delegate (ApiFile file, string s)
+ {
+ if (fs != null)
+ fs.Close();
+ if (onError != null)
+ onError(s);
+ };
+
+ // query multipart upload status.
+ // we might be resuming a previous upload
+ ApiFile.UploadStatus uploadStatus = null;
+ {
+ while (true)
+ {
+ bool wait = true;
+ string errorStr = "";
+ bool worthRetry = false;
+
+ apiFile.GetUploadStatus(apiFile.GetLatestVersionNumber(), fileDescriptorType,
+ (c) =>
+ {
+ uploadStatus = c.Model as ApiFile.UploadStatus;
+ wait = false;
+
+ VRC.Core.Logger.Log("Found existing multipart upload status (next part = " + uploadStatus.nextPartNumber + ")", DebugLevel.All);
+ },
+ (c) =>
+ {
+ errorStr = "Failed to query multipart upload status: " + c.Error;
+ wait = false;
+ if (c.Code == 400)
+ worthRetry = true;
+ });
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ if (onError != null)
+ onError(errorStr);
+ if (!worthRetry)
+ yield break;
+ }
+
+ if (!worthRetry)
+ break;
+ }
+ }
+
+ // split file into chunks
+ try
+ {
+ fs = File.OpenRead(filename);
+ }
+ catch (Exception e)
+ {
+ if (onError != null)
+ onError("Couldn't open file: " + e.Message);
+ yield break;
+ }
+
+ byte[] buffer = new byte[kMultipartUploadChunkSize * 2];
+
+ long totalBytesUploaded = 0;
+ List<string> etags = new List<string>();
+ if (uploadStatus != null)
+ etags = uploadStatus.etags.ToList();
+
+ int numParts = Mathf.Max(1, Mathf.FloorToInt((float)fs.Length / (float)kMultipartUploadChunkSize));
+ for (int partNumber = 1; partNumber <= numParts; partNumber++)
+ {
+ // read chunk
+ int bytesToRead = partNumber < numParts ? kMultipartUploadChunkSize : (int)(fs.Length - fs.Position);
+ int bytesRead = 0;
+ try
+ {
+ bytesRead = fs.Read(buffer, 0, bytesToRead);
+ }
+ catch (Exception e)
+ {
+ fs.Close();
+ if (onError != null)
+ onError("Couldn't read file: " + e.Message);
+ yield break;
+ }
+
+ if (bytesRead != bytesToRead)
+ {
+ fs.Close();
+ if (onError != null)
+ onError("Couldn't read file: read incorrect number of bytes from stream");
+ yield break;
+ }
+
+ // check if this part has been upload already
+ // NOTE: uploadStatus.nextPartNumber == number of parts already uploaded
+ if (uploadStatus != null && partNumber <= uploadStatus.nextPartNumber)
+ {
+ totalBytesUploaded += bytesRead;
+ continue;
+ }
+
+ // start upload
+ string uploadUrl = "";
+
+ while (true)
+ {
+ bool wait = true;
+ string errorStr = "";
+ bool worthRetry = false;
+
+ apiFile.StartMultipartUpload(fileDescriptorType, partNumber,
+ (c) =>
+ {
+ uploadUrl = (c as ApiDictContainer).ResponseDictionary["url"] as string;
+ wait = false;
+ },
+ (c) =>
+ {
+ errorStr = "Failed to start part upload: " + c.Error;
+ wait = false;
+ });
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ fs.Close();
+ if (onError != null)
+ onError(errorStr);
+ if (!worthRetry)
+ yield break;
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+
+ // PUT file part
+ {
+ bool wait = true;
+ string errorStr = "";
+
+ VRC.HttpRequest req = ApiFile.PutMultipartDataToURL(uploadUrl, buffer, bytesRead, GetMimeTypeFromExtension(Path.GetExtension(filename)), true,
+ delegate (string etag)
+ {
+ if (!string.IsNullOrEmpty(etag))
+ etags.Add(etag);
+ totalBytesUploaded += bytesRead;
+ wait = false;
+ },
+ delegate (string error)
+ {
+ errorStr = "Failed to upload data: " + error;
+ wait = false;
+ },
+ delegate (long uploaded, long length)
+ {
+ if (onProgress != null)
+ onProgress(totalBytesUploaded + uploaded, fileSize);
+ }
+ );
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ if (req != null)
+ req.Abort();
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ fs.Close();
+ if (onError != null)
+ onError(errorStr);
+ yield break;
+ }
+ }
+ }
+
+ // finish upload
+ while (true)
+ {
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ bool wait = true;
+ string errorStr = "";
+ bool worthRetry = false;
+
+ apiFile.FinishUpload(fileDescriptorType, etags,
+ (c) =>
+ {
+ apiFile = c.Model as ApiFile;
+ wait = false;
+ },
+ (c) =>
+ {
+ errorStr = "Failed to finish upload: " + c.Error;
+ wait = false;
+ if (c.Code == 400)
+ worthRetry = true;
+ });
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ fs.Close();
+ if (onError != null)
+ onError(errorStr);
+ if (!worthRetry)
+ yield break;
+ }
+
+ // delay to let write get through servers
+ yield return new WaitForSecondsRealtime(kPostWriteDelay);
+
+ if (!worthRetry)
+ break;
+ }
+
+ fs.Close();
+ }
+
+ private IEnumerator UploadFileComponentVerifyRecord(ApiFile apiFile,
+ ApiFile.Version.FileDescriptor.Type fileDescriptorType,
+ string filename,
+ string md5Base64,
+ long fileSize,
+ ApiFile.Version.FileDescriptor fileDesc,
+ Action<ApiFile> onSuccess,
+ Action<string> onError,
+ Action<long, long> onProgress,
+ FileOpCancelQuery cancelQuery)
+ {
+ OnFileOpError onCancelFunc = delegate (ApiFile file, string s)
+ {
+ if (onError != null)
+ onError(s);
+ };
+
+ float initialStartTime = Time.realtimeSinceStartup;
+ float startTime = initialStartTime;
+ float timeout = GetServerProcessingWaitTimeoutForDataSize(fileDesc.sizeInBytes);
+ float waitDelay = SERVER_PROCESSING_INITIAL_RETRY_TIME;
+ float maxDelay = SERVER_PROCESSING_MAX_RETRY_TIME;
+
+ while (true)
+ {
+ if (apiFile == null)
+ {
+ if (onError != null)
+ onError("ApiFile is null");
+ yield break;
+ }
+
+ var desc = apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), fileDescriptorType);
+ if (desc == null)
+ {
+ if (onError != null)
+ onError("File descriptor is null ('" + fileDescriptorType + "')");
+ yield break;
+ }
+
+ if (desc.status != ApiFile.Status.Waiting)
+ {
+ // upload completed or is processing
+ break;
+ }
+
+ // wait for next poll
+ while (Time.realtimeSinceStartup - startTime < waitDelay)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+
+ if (Time.realtimeSinceStartup - initialStartTime > timeout)
+ {
+ if (onError != null)
+ onError("Couldn't verify upload status: Timed out wait for server processing");
+ yield break;
+ }
+
+ yield return null;
+ }
+
+
+ while (true)
+ {
+ bool wait = true;
+ string errorStr = "";
+ bool worthRetry = false;
+
+ apiFile.Refresh(
+ (c) =>
+ {
+ wait = false;
+ },
+ (c) =>
+ {
+ errorStr = "Couldn't verify upload status: " + c.Error;
+ wait = false;
+ if (c.Code == 400)
+ worthRetry = true;
+ });
+
+ while (wait)
+ {
+ if (CheckCancelled(cancelQuery, onCancelFunc, apiFile))
+ {
+ yield break;
+ }
+
+ yield return null;
+ }
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ if (onError != null)
+ onError(errorStr);
+ if (!worthRetry)
+ yield break;
+ }
+
+ if (!worthRetry)
+ break;
+ }
+
+ waitDelay = Mathf.Min(waitDelay * 2, maxDelay);
+ startTime = Time.realtimeSinceStartup;
+ }
+
+ if (onSuccess != null)
+ onSuccess(apiFile);
+ }
+
+ private IEnumerator UploadFileComponentInternal(ApiFile apiFile,
+ ApiFile.Version.FileDescriptor.Type fileDescriptorType,
+ string filename,
+ string md5Base64,
+ long fileSize,
+ Action<ApiFile> onSuccess,
+ Action<string> onError,
+ Action<long, long> onProgress,
+ FileOpCancelQuery cancelQuery)
+ {
+ VRC.Core.Logger.Log("UploadFileComponent: " + fileDescriptorType + " (" + apiFile.id + "): " + filename, DebugLevel.All);
+ ApiFile.Version.FileDescriptor fileDesc = apiFile.GetFileDescriptor(apiFile.GetLatestVersionNumber(), fileDescriptorType);
+
+ if (!UploadFileComponentValidateFileDesc(apiFile, filename, md5Base64, fileSize, fileDesc, onSuccess, onError))
+ yield break;
+
+ switch (fileDesc.category)
+ {
+ case ApiFile.Category.Simple:
+ yield return UploadFileComponentDoSimpleUpload(apiFile, fileDescriptorType, filename, md5Base64, fileSize, onSuccess, onError, onProgress, cancelQuery);
+ break;
+ case ApiFile.Category.Multipart:
+ yield return UploadFileComponentDoMultipartUpload(apiFile, fileDescriptorType, filename, md5Base64, fileSize, onSuccess, onError, onProgress, cancelQuery);
+ break;
+ default:
+ if (onError != null)
+ onError("Unknown file category type: " + fileDesc.category);
+ yield break;
+ }
+
+ yield return UploadFileComponentVerifyRecord(apiFile, fileDescriptorType, filename, md5Base64, fileSize, fileDesc, onSuccess, onError, onProgress, cancelQuery);
+ }
+ }
+}
+
+#endif
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs.meta
new file mode 100644
index 00000000..29e6938a
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ApiFileHelper.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 36c0d886a26373c46be857f2fc441071
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs
new file mode 100644
index 00000000..108bd7e2
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs
@@ -0,0 +1,20 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace VRC.SDKBase
+{
+ public class AudioManagerSettings
+ {
+ public const float MinVoiceSendDistance = 25.0f; // meters
+ public const float MaxVoiceSendDistancePctOfFarRange = 0.5f;
+ public const float VoiceFadeOutDistancePctOfFarRange = 0.25f;
+ public const float RoomAudioGain = 10f; // dB
+ public const float RoomAudioMaxRange = 80f; // meters
+ public const float VoiceGain = 15f; // dB
+ public const float VoiceMaxRange = 25f; // meters, this is half the oculus inv sq max range
+ public const float LipsyncGain = 1f; // multiplier, not dB!
+ public const float AvatarAudioMaxGain = 10f; // dB
+ public const float AvatarAudioMaxRange = 40f; // meters
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs.meta
new file mode 100644
index 00000000..91be94d7
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/AudioManagerSettings.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: acadc6659c5ab3446ad0d5de2563f95f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs
new file mode 100644
index 00000000..31ec162d
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs
@@ -0,0 +1,20 @@
+using UnityEngine;
+
+namespace VRCSDK2
+{
+ public class CommunityLabsConstants
+ {
+ public const string COMMUNITY_LABS_DOCUMENTATION_URL = "https://docs.vrchat.com/docs/vrchat-community-labs";
+
+ public const string MANAGE_WORLD_IN_BROWSER_STRING = "Manage World in Browser";
+ public const string READ_COMMUNITY_LABS_DOCS_STRING = "Read Community Labs Docs";
+
+ public const string UPLOADED_CONTENT_SUCCESSFULLY_MESSAGE = "Content Successfully Uploaded!";
+
+ public const string UPLOADED_NEW_PRIVATE_WORLD_CONFIRMATION_MESSAGE = "You've uploaded a private world. You can find it on the \"Mine\" row in your worlds menu.";
+ public const string UPDATED_PRIVATE_WORLD_CONFIRMATION_MESSAGE = "You've updated your private world. You can find it on the \"Mine\" row in your worlds menu.";
+ public const string PUBLISHED_WORLD_TO_COMMUNITY_LABS_CONFIRMATION_MESSAGE = "You've uploaded and published a world to Community Labs.";
+ public const string UPDATED_COMMUNITY_LABS_WORLD_CONFIRMATION_MESSAGE = "You've updated your Community Labs world.";
+ public const string UPDATED_PUBLIC_WORLD_CONFIRMATION_MESSAGE = "You've updated your public world.";
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs.meta
new file mode 100644
index 00000000..5a616c22
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/CommunityLabsConstants.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8d047eaa3325d654aa62ccad6f73eb93
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs
new file mode 100644
index 00000000..2d136979
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs
@@ -0,0 +1,133 @@
+using System.Collections;
+using System.Collections.Generic;
+using System;
+using System.IO;
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.SceneManagement;
+using VRC.Core;
+#if UNITY_EDITOR
+using UnityEditor;
+
+namespace VRCSDK2
+{
+ public class ContentUploadedDialog : EditorWindow
+ {
+ private Texture2D clIconGraphic = null;
+ private Color dialogTextColor = Color.white;
+ private string contentUrl = null;
+
+ private void OnEnable()
+ {
+ if(EditorGUIUtility.isProSkin)
+ dialogTextColor = Color.white;
+ else
+ dialogTextColor = Color.black;
+
+ clIconGraphic = Resources.Load("vrcSdkClDialogNewIcon") as Texture2D;
+ }
+
+ public void setContentURL(string url = null)
+ {
+ contentUrl = url;
+ }
+
+ void OnGUI()
+ {
+ const int CONTENT_UPLOADED_BORDER_SIZE = 20;
+ const int CONTENT_UPLOADED_HORIZONTAL_SPACING = 10;
+
+ const int CONTENT_UPLOADED_BUTTON_WIDTH = 260;
+ const int CONTENT_UPLOADED_BUTTON_HEIGHT = 40;
+ const int CONTENT_CL_VERTICAL_HEADER_SPACING = 40;
+
+ const int CONTENT_CL_TEXT_REGION_HEIGHT = 120;
+
+ const int CONTENT_MIN_WINDOW_WIDTH = (CONTENT_UPLOADED_BUTTON_WIDTH * 2) + CONTENT_UPLOADED_HORIZONTAL_SPACING + (CONTENT_UPLOADED_BORDER_SIZE * 2);
+ const int CONTENT_MIN_WINDOW_HEIGHT = CONTENT_UPLOADED_BUTTON_HEIGHT + CONTENT_CL_VERTICAL_HEADER_SPACING + CONTENT_CL_TEXT_REGION_HEIGHT + (CONTENT_UPLOADED_BORDER_SIZE * 2);
+
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(CONTENT_UPLOADED_BORDER_SIZE);
+
+ // Community Labs graphic
+ if (RuntimeWorldCreation.IsCurrentWorldInCommunityLabs && (null != clIconGraphic))
+ {
+ GUILayout.Label(new GUIContent(clIconGraphic), GUIStyle.none);
+ }
+
+ this.minSize = new Vector2(CONTENT_MIN_WINDOW_WIDTH, CONTENT_MIN_WINDOW_HEIGHT);
+
+ GUILayout.BeginVertical();
+ if (RuntimeWorldCreation.IsCurrentWorldInCommunityLabs && (null != clIconGraphic))
+ GUILayout.Space(CONTENT_CL_VERTICAL_HEADER_SPACING);
+ GUIStyle uploadedTitleStyle = new GUIStyle(EditorStyles.boldLabel);
+ uploadedTitleStyle.normal.textColor = dialogTextColor;
+ uploadedTitleStyle.fontSize = 15;
+ GUILayout.Label(CommunityLabsConstants.UPLOADED_CONTENT_SUCCESSFULLY_MESSAGE, uploadedTitleStyle);
+
+ string uploadedMessage = CommunityLabsConstants.UPLOADED_NEW_PRIVATE_WORLD_CONFIRMATION_MESSAGE;
+
+ if (!RuntimeWorldCreation.IsCurrentWorldUploaded)
+ {
+ if (RuntimeWorldCreation.IsCurrentWorldInCommunityLabs)
+ uploadedMessage = CommunityLabsConstants.PUBLISHED_WORLD_TO_COMMUNITY_LABS_CONFIRMATION_MESSAGE;
+ else
+ uploadedMessage = CommunityLabsConstants.UPLOADED_NEW_PRIVATE_WORLD_CONFIRMATION_MESSAGE;
+ }
+ else
+ {
+ if (RuntimeWorldCreation.IsCurrentWorldInCommunityLabs)
+ {
+ uploadedMessage = CommunityLabsConstants.UPDATED_COMMUNITY_LABS_WORLD_CONFIRMATION_MESSAGE;
+ }
+ else
+ {
+ if (RuntimeWorldCreation.IsCurrentWorldPubliclyPublished)
+ uploadedMessage = CommunityLabsConstants.UPDATED_PUBLIC_WORLD_CONFIRMATION_MESSAGE;
+ else
+ uploadedMessage = CommunityLabsConstants.UPDATED_PRIVATE_WORLD_CONFIRMATION_MESSAGE;
+ }
+ }
+
+ GUIStyle uploadedMessageStyle = new GUIStyle(EditorStyles.label);
+ uploadedMessageStyle.normal.textColor = dialogTextColor;
+ uploadedMessageStyle.fontSize = 13;
+ uploadedMessageStyle.wordWrap = true;
+ GUILayout.Label(uploadedMessage, uploadedMessageStyle);
+ GUILayout.EndVertical();
+
+ GUILayout.EndHorizontal();
+
+ GUILayout.FlexibleSpace();
+
+ GUILayout.BeginHorizontal();
+
+ GUILayout.Space(CONTENT_UPLOADED_BORDER_SIZE);
+
+ if (RuntimeWorldCreation.IsCurrentWorldInCommunityLabs)
+ {
+ if (GUILayout.Button(CommunityLabsConstants.READ_COMMUNITY_LABS_DOCS_STRING, GUILayout.Width(CONTENT_UPLOADED_BUTTON_WIDTH), GUILayout.Height(CONTENT_UPLOADED_BUTTON_HEIGHT)))
+ {
+ Application.OpenURL(CommunityLabsConstants.COMMUNITY_LABS_DOCUMENTATION_URL);
+ }
+ }
+
+ GUILayout.FlexibleSpace();
+
+ if (GUILayout.Button(CommunityLabsConstants.MANAGE_WORLD_IN_BROWSER_STRING, GUILayout.Width(CONTENT_UPLOADED_BUTTON_WIDTH), GUILayout.Height(CONTENT_UPLOADED_BUTTON_HEIGHT)))
+ {
+ Application.OpenURL(contentUrl);
+ }
+
+ if (RuntimeWorldCreation.IsCurrentWorldInCommunityLabs)
+ GUILayout.Space(CONTENT_UPLOADED_BORDER_SIZE);
+ else
+ GUILayout.FlexibleSpace();
+
+ GUILayout.EndHorizontal();
+
+ GUILayout.Space(CONTENT_UPLOADED_BORDER_SIZE);
+ }
+ }
+}
+#endif
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs.meta
new file mode 100644
index 00000000..b589cde9
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/ContentUploadedDialog.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e1c693656bf5d584b87df969efeb5536
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs
new file mode 100644
index 00000000..50504090
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Reflection;
+
+namespace VRC. SDKBase
+{
+ public static class GameViewMethods
+ {
+ private static readonly Type GameViewType = System.Type.GetType("UnityEditor.GameView,UnityEditor");
+ private static readonly Type PlayModeViewType = System.Type.GetType("UnityEditor.PlayModeView, UnityEditor");
+
+ public static int GetSelectedSizeIndex()
+ {
+ return (int) GetSelectedSizeProperty().GetValue(GetPlayModeViewObject());
+ }
+
+ public static void SetSelectedSizeIndex(int value)
+ {
+ var selectedSizeIndexProp = GetSelectedSizeProperty();
+ selectedSizeIndexProp.SetValue(GetPlayModeViewObject(), value, null);
+ }
+
+ // Set it to something else just to force a refresh
+ public static void ResizeGameView()
+ {
+ int current = GetSelectedSizeIndex();
+ SetSelectedSizeIndex(current == 0 ? 1 : 0);
+ }
+
+ private static PropertyInfo GetSelectedSizeProperty()
+ {
+ return GameViewType.GetProperty("selectedSizeIndex",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ }
+
+ private static Object GetPlayModeViewObject()
+ {
+ MethodInfo GetMainPlayModeView = PlayModeViewType.GetMethod("GetMainPlayModeView",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+ return GetMainPlayModeView.Invoke(null, null);
+ }
+
+ public static void Repaint()
+ {
+ MethodInfo RepaintAll = PlayModeViewType.GetMethod("RepaintAll", BindingFlags.NonPublic | BindingFlags.Static);
+ RepaintAll.Invoke(GetPlayModeViewObject(), null);
+ }
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs.meta
new file mode 100644
index 00000000..38eb991a
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/GameViewMethods.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d0c461e358764cd1ab95544e34b0346c
+timeCreated: 1627603592 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback.meta
new file mode 100644
index 00000000..c943a0ce
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 546dc9c02d5c61d438c41f413d3cf866
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs
new file mode 100644
index 00000000..5a65be9b
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+public class FallbackMaterialCache
+{
+ private readonly Dictionary<Material, Material> _fallbackMaterialCache = new Dictionary<Material, Material>();
+
+ public void AddFallbackMaterial(Material material, Material fallbackMaterial)
+ {
+ if(!_fallbackMaterialCache.ContainsKey(material))
+ {
+ _fallbackMaterialCache.Add(material, fallbackMaterial);
+ }
+ else
+ {
+ Debug.LogError($"Attempted to add a duplicate fallback material '{fallbackMaterial.name}' for original material '{material.name}'.");
+ }
+ }
+
+ public bool TryGetFallbackMaterial(Material material, out Material fallbackMaterial)
+ {
+ if(material != null)
+ {
+ return _fallbackMaterialCache.TryGetValue(material, out fallbackMaterial);
+ }
+
+ fallbackMaterial = null;
+ return false;
+ }
+
+ public void Clear()
+ {
+ Material[] cachedFallbackMaterials = _fallbackMaterialCache.Values.ToArray();
+ for(int i = cachedFallbackMaterials.Length - 1; i >= 0; i--)
+ {
+ Object.Destroy(cachedFallbackMaterials[i]);
+ }
+
+ _fallbackMaterialCache.Clear();
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs.meta
new file mode 100644
index 00000000..64637629
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/MaterialFallback/FallbackMaterialCache.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 10121679f780956408f9a434a526f553
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs
new file mode 100644
index 00000000..154fba15
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs
@@ -0,0 +1,324 @@
+using System.Collections;
+using System.Collections.Generic;
+using System;
+using System.IO;
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.SceneManagement;
+using VRC.Core;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace VRCSDK2
+{
+#if UNITY_EDITOR
+
+ public class RuntimeAPICreation : MonoBehaviour
+ {
+ public VRC.Core.PipelineManager pipelineManager;
+
+ protected bool forceNewFileCreation = false;
+ protected bool useFileApi = false;
+
+ protected bool isUploading = false;
+ protected float uploadProgress = 0f;
+ protected string uploadMessage;
+ protected string uploadTitle;
+
+ protected string uploadVrcPath;
+ protected string uploadUnityPackagePath;
+
+ protected string cloudFrontAssetUrl;
+ protected string cloudFrontImageUrl;
+ protected string cloudFrontUnityPackageUrl;
+
+ protected CameraImageCapture imageCapture;
+
+ private bool cancelRequested = false;
+
+ public static bool publishingToCommunityLabs = false;
+
+ private Dictionary<string, string> mRetryState = new Dictionary<string, string>();
+
+ protected bool isUpdate { get { return pipelineManager.completedSDKPipeline; } }
+
+ protected void Start()
+ {
+ if (!Application.isEditor || !Application.isPlaying)
+ return;
+
+ PipelineSaver ps = GameObject.FindObjectOfType<PipelineSaver>();
+ pipelineManager = ps.gameObject.GetComponent<PipelineManager>();
+
+ imageCapture = GetComponent<CameraImageCapture>();
+ imageCapture.shotCamera = GameObject.Find("VRCCam").GetComponent<Camera>();
+
+ LoadUploadRetryStateFromCache();
+
+ forceNewFileCreation = UnityEditor.EditorPrefs.GetBool("forceNewFileCreation", true);
+ useFileApi = UnityEditor.EditorPrefs.GetBool("useFileApi", false);
+
+ API.SetOnlineMode(true);
+ }
+
+ protected void Update()
+ {
+ if (isUploading)
+ {
+ bool cancelled = UnityEditor.EditorUtility.DisplayCancelableProgressBar(uploadTitle, uploadMessage, uploadProgress);
+ if (cancelled)
+ {
+ cancelRequested = true;
+ }
+
+ if (EditorApplication.isPaused)
+ EditorApplication.isPaused = false;
+
+ }
+ }
+
+ protected void LoadUploadRetryStateFromCache()
+ {
+ try
+ {
+ string json = File.ReadAllText(GetUploadRetryStateFilePath());
+ mRetryState = VRC.Tools.ObjDictToStringDict(VRC.Tools.JsonDecode(json) as Dictionary<string, object>);
+
+ Debug.LogFormat("<color=yellow> loaded retry state: {0}</color>", json);
+ }
+ catch (Exception)
+ {
+ // normal case
+ return;
+ }
+
+ Debug.Log("Loaded upload retry state from: " + GetUploadRetryStateFilePath());
+ }
+
+ protected void SaveUploadRetryState(string key, string val)
+ {
+ if (string.IsNullOrEmpty(val))
+ return;
+ mRetryState[key] = val;
+ SaveUploadRetryState();
+ }
+
+ protected void SaveUploadRetryState()
+ {
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(GetUploadRetryStateFilePath()));
+ string json = VRC.Tools.JsonEncode(mRetryState);
+ File.WriteAllText(GetUploadRetryStateFilePath(), json);
+
+ Debug.LogFormat("<color=yellow> wrote retry state: {0}</color>", json);
+ }
+ catch (Exception e)
+ {
+ Debug.LogError("Couldn't save upload retry state: " + GetUploadRetryStateFilePath() + "\n" + e.Message);
+ return;
+ }
+
+ Debug.Log("Saved upload retry state to: " + GetUploadRetryStateFilePath());
+ }
+
+ protected void ClearUploadRetryState()
+ {
+ try
+ {
+ if (!File.Exists(GetUploadRetryStateFilePath()))
+ return;
+
+ File.Delete(GetUploadRetryStateFilePath());
+ }
+ catch (Exception e)
+ {
+ Debug.LogError("Couldn't delete upload retry state: " + GetUploadRetryStateFilePath() + "\n" + e.Message);
+ return;
+ }
+
+ Debug.Log("Cleared upload retry state at: " + GetUploadRetryStateFilePath());
+ }
+
+ protected string GetUploadRetryStateFilePath()
+ {
+ string id = UnityEditor.AssetDatabase.AssetPathToGUID(SceneManager.GetActiveScene().path);
+ return Path.Combine(VRC.Tools.GetTempFolderPath(id), "upload_retry.dat");
+ }
+
+ protected string GetUploadRetryStateValue(string key)
+ {
+ return mRetryState.ContainsKey(key) ? mRetryState[key] : "";
+ }
+
+ protected virtual void DisplayUpdateCompletedDialog(string contentUrl=null)
+ {
+ if (UnityEditor.EditorUtility.DisplayDialog("VRChat SDK", "Update Complete! Launch VRChat to see your uploaded content." + (null==contentUrl ? "" : "\n\nManage content at: " + contentUrl ), (null == contentUrl) ? "Okay" : CommunityLabsConstants.MANAGE_WORLD_IN_BROWSER_STRING, (null == contentUrl) ? "" : "Done" ))
+ {
+ if (null!=contentUrl)
+ {
+ Application.OpenURL(contentUrl);
+ }
+ }
+ }
+
+ protected void OnSDKPipelineComplete(string contentUrl=null)
+ {
+ VRC.Core.Logger.Log("OnSDKPipelineComplete", DebugLevel.All);
+ isUploading = false;
+ pipelineManager.completedSDKPipeline = true;
+ ClearUploadRetryState();
+ UnityEditor.EditorPrefs.SetBool("forceNewFileCreation", false);
+ UnityEditor.EditorApplication.isPaused = false;
+ UnityEditor.EditorApplication.isPlaying = false;
+ UnityEditor.EditorUtility.ClearProgressBar();
+ DisplayUpdateCompletedDialog(contentUrl);
+ }
+
+ protected void OnSDKPipelineError(string error, string details)
+ {
+ VRC.Core.Logger.Log("OnSDKPipelineError: " + error + " - " + details, DebugLevel.All);
+ isUploading = false;
+ pipelineManager.completedSDKPipeline = true;
+ UnityEditor.EditorApplication.isPaused = false;
+ UnityEditor.EditorApplication.isPlaying = false;
+ UnityEditor.EditorUtility.ClearProgressBar();
+ if (cancelRequested)
+ UnityEditor.EditorUtility.DisplayDialog("VRChat SDK", "The update was cancelled.", "Okay");
+ else
+ UnityEditor.EditorUtility.DisplayDialog("VRChat SDK", "Error updating content. " + error + "\n" + details, "Okay");
+ }
+
+ protected void SetUploadProgress(string title, string message, float progress)
+ {
+ uploadTitle = title;
+ uploadMessage = message;
+ uploadProgress = progress;
+ }
+
+ protected bool WasCancelRequested(ApiFile apiFile)
+ {
+ return cancelRequested;
+ }
+
+ protected void PrepareUnityPackageForS3(string packagePath, string blueprintId, int version, AssetVersion assetVersion)
+ {
+ uploadUnityPackagePath = Application.temporaryCachePath + "/" + blueprintId + "_" + version.ToString() + "_" + Application.unityVersion + "_" + assetVersion.ApiVersion + "_" + VRC.Tools.Platform +
+ "_" + API.GetServerEnvironmentForApiUrl() + ".unitypackage";
+ uploadUnityPackagePath.Trim();
+ uploadUnityPackagePath.Replace(' ', '_');
+
+ if (System.IO.File.Exists(uploadUnityPackagePath))
+ System.IO.File.Delete(uploadUnityPackagePath);
+
+ System.IO.File.Copy(packagePath, uploadUnityPackagePath);
+ }
+
+ protected void PrepareVRCPathForS3(string abPath, string blueprintId, int version, AssetVersion assetVersion)
+ {
+ uploadVrcPath = Application.temporaryCachePath + "/" + blueprintId + "_" + version.ToString() + "_" + Application.unityVersion + "_" + assetVersion.ApiVersion + "_" + VRC.Tools.Platform + "_" + API.GetServerEnvironmentForApiUrl() + System.IO.Path.GetExtension(abPath);
+ uploadVrcPath.Trim();
+ uploadVrcPath.Replace(' ', '_');
+
+ if (System.IO.File.Exists(uploadVrcPath))
+ System.IO.File.Delete(uploadVrcPath);
+
+ System.IO.File.Copy(abPath, uploadVrcPath);
+ }
+
+ protected IEnumerator UploadFile(string filename, string existingFileUrl, string friendlyFilename, string fileType, Action<string> onSuccess)
+ {
+ if (string.IsNullOrEmpty(filename))
+ yield break;
+ VRC.Core.Logger.Log("Uploading " + fileType + "(" + filename + ") ...", DebugLevel.All);
+ SetUploadProgress("Uploading " + fileType + "...", "", 0.0f);
+
+ string fileId = GetUploadRetryStateValue(filename);
+ if (string.IsNullOrEmpty(fileId))
+ fileId = isUpdate ? ApiFile.ParseFileIdFromFileAPIUrl(existingFileUrl) : "";
+ string errorStr = "";
+ string newFileUrl = "";
+
+
+ yield return StartCoroutine(ApiFileHelper.Instance.UploadFile(filename, forceNewFileCreation ? "" : fileId, friendlyFilename,
+ delegate (ApiFile apiFile, string message)
+ {
+ newFileUrl = apiFile.GetFileURL();
+ if (VRC.Core.Logger.DebugLevelIsEnabled(DebugLevel.API))
+ VRC.Core.Logger.Log(fileType + " upload succeeded: " + message + " (" + filename +
+ ") => " + apiFile.ToString(), DebugLevel.API);
+ else
+ VRC.Core.Logger.Log(fileType + " upload succeeded ", DebugLevel.Always);
+ },
+ delegate (ApiFile apiFile, string error)
+ {
+ SaveUploadRetryState(filename, apiFile.id);
+
+ errorStr = error;
+ Debug.LogError(fileType + " upload failed: " + error + " (" + filename +
+ ") => " + apiFile.ToString());
+ },
+ delegate (ApiFile apiFile, string status, string subStatus, float pct)
+ {
+ SetUploadProgress("Uploading " + fileType + "...", status + (!string.IsNullOrEmpty(subStatus) ? " (" + subStatus + ")" : ""), pct);
+ },
+ WasCancelRequested
+ ));
+
+ if (!string.IsNullOrEmpty(errorStr))
+ {
+ OnSDKPipelineError(fileType + " upload failed.", errorStr);
+ yield break;
+ }
+
+ if (onSuccess != null)
+ onSuccess(newFileUrl);
+ }
+
+ protected IEnumerator UpdateImage(string existingFileUrl, string friendlyFileName)
+ {
+ string imagePath = imageCapture.TakePicture();
+
+ if (!string.IsNullOrEmpty(imagePath))
+ {
+ yield return StartCoroutine(UploadFile(imagePath, existingFileUrl, friendlyFileName, "Image",
+ delegate (string fileUrl)
+ {
+ cloudFrontImageUrl = fileUrl;
+ }
+ ));
+ }
+ }
+
+ protected virtual IEnumerator CreateBlueprint()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected virtual IEnumerator UpdateBlueprint()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected bool ValidateNameInput(InputField nameInput)
+ {
+ bool isValid = true;
+ if (string.IsNullOrEmpty(nameInput.text))
+ {
+ isUploading = false;
+ UnityEditor.EditorUtility.DisplayDialog("Invalid Input", "Cannot leave the name field empty.", "OK");
+ isValid = false;
+ }
+ return isValid;
+ }
+
+ protected bool ValidateAssetBundleBlueprintID(string blueprintID)
+ {
+ string lastBuiltID = UnityEditor.EditorPrefs.GetString("lastBuiltAssetBundleBlueprintID", "");
+ return !string.IsNullOrEmpty(lastBuiltID) && lastBuiltID == blueprintID;
+ }
+ }
+#endif
+
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs.meta
new file mode 100644
index 00000000..a6fb7e10
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeAPICreation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a3132e0ab7e16494a9d492087a1ca447
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs
new file mode 100644
index 00000000..7017dbae
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs
@@ -0,0 +1,421 @@
+using UnityEngine;
+using UnityEngine.UI;
+using System.Collections;
+using System.Collections.Generic;
+using VRC.Core;
+using VRC.SDKBase;
+
+namespace VRCSDK2
+{
+#if UNITY_EDITOR
+ public class RuntimeBlueprintCreation : RuntimeAPICreation
+ {
+ public GameObject waitingPanel;
+ public GameObject blueprintPanel;
+ public GameObject errorPanel;
+
+ public Text titleText;
+ public InputField blueprintName;
+ public InputField blueprintDescription;
+ public RawImage bpImage;
+ public Image liveBpImage;
+ public Toggle shouldUpdateImageToggle;
+ public Toggle contentSex;
+ public Toggle contentViolence;
+ public Toggle contentGore;
+ public Toggle contentOther;
+ public Toggle developerAvatar;
+ public Toggle sharePrivate;
+ public Toggle sharePublic;
+ public Toggle tagFallback;
+
+ public UnityEngine.UI.Button uploadButton;
+
+ private ApiAvatar apiAvatar;
+
+ new void Start()
+ {
+ if (!Application.isEditor || !Application.isPlaying)
+ return;
+
+ base.Start();
+
+ var desc = pipelineManager.GetComponent<VRC.SDKBase.VRC_AvatarDescriptor>();
+ desc.PositionPortraitCamera(imageCapture.shotCamera.transform);
+
+ Application.runInBackground = true;
+ UnityEngine.XR.XRSettings.enabled = false;
+
+ uploadButton.onClick.AddListener(SetupUpload);
+
+ shouldUpdateImageToggle.onValueChanged.AddListener(ToggleUpdateImage);
+
+ Login();
+ }
+
+ void LoginErrorCallback(string obj)
+ {
+ VRC.Core.Logger.LogError("Could not log in - " + obj, DebugLevel.Always);
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(true);
+ }
+
+ void Login()
+ {
+ if (!ApiCredentials.Load())
+ LoginErrorCallback("Not logged in");
+ else
+ APIUser.InitialFetchCurrentUser(
+ delegate (ApiModelContainer<APIUser> c)
+ {
+ pipelineManager.user = c.Model as APIUser;
+
+ ApiAvatar av = new ApiAvatar() { id = pipelineManager.blueprintId };
+ av.Get(false,
+ (c2) =>
+ {
+ VRC.Core.Logger.Log("<color=magenta>Updating an existing avatar.</color>", DebugLevel.API);
+ apiAvatar = c2.Model as ApiAvatar;
+ pipelineManager.completedSDKPipeline = !string.IsNullOrEmpty(apiAvatar.authorId);
+ SetupUI();
+ },
+ (c2) =>
+ {
+ VRC.Core.Logger.Log("<color=magenta>Creating a new avatar.</color>", DebugLevel.API);
+ apiAvatar = new ApiAvatar();
+ apiAvatar.id = pipelineManager.blueprintId;
+ pipelineManager.completedSDKPipeline = !string.IsNullOrEmpty(apiAvatar.authorId);
+ SetupUI();
+ });
+ }, (c) => {
+ LoginErrorCallback(c.Error);
+ });
+ }
+
+ void SetupUI()
+ {
+ if (!ValidateAssetBundleBlueprintID(apiAvatar.id))
+ {
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(true);
+ OnSDKPipelineError("The asset bundle is out of date. Please rebuild the scene using 'New Build'.", "The blueprint ID in the scene does not match the id in the asset bundle.");
+ return;
+ }
+
+ if (APIUser.Exists(pipelineManager.user))
+ {
+ waitingPanel.SetActive(false);
+ blueprintPanel.SetActive(true);
+ errorPanel.SetActive(false);
+
+ if (isUpdate)
+ {
+ // bp update
+ if (apiAvatar.authorId == pipelineManager.user.id)
+ {
+ titleText.text = "Update Avatar";
+ // apiAvatar = pipelineManager.user.GetBlueprint(pipelineManager.blueprintId) as ApiAvatar;
+ blueprintName.text = apiAvatar.name;
+ contentSex.isOn = apiAvatar.tags.Contains("content_sex");
+ contentViolence.isOn = apiAvatar.tags.Contains("content_violence");
+ contentGore.isOn = apiAvatar.tags.Contains("content_gore");
+ contentOther.isOn = apiAvatar.tags.Contains("content_other");
+ developerAvatar.isOn = apiAvatar.tags.Contains("developer");
+ sharePrivate.isOn = apiAvatar.releaseStatus.Contains("private");
+ sharePublic.isOn = apiAvatar.releaseStatus.Contains("public");
+
+ tagFallback.isOn = apiAvatar.tags.Contains("author_quest_fallback");
+ tagFallback.transform.parent.gameObject.SetActive(true);
+
+ switch (pipelineManager.fallbackStatus)
+ {
+ case PipelineManager.FallbackStatus.Valid:
+#if UNITY_ANDROID
+ tagFallback.interactable = true;
+ tagFallback.GetComponentInChildren<Text>().text = "Use for Fallback";
+#else
+ tagFallback.interactable = false;
+ tagFallback.GetComponentInChildren<Text>().text = "Use for Fallback (change only with Android upload)";
+#endif
+ break;
+ case PipelineManager.FallbackStatus.InvalidPerformance:
+ case PipelineManager.FallbackStatus.InvalidRig:
+ tagFallback.isOn = false; // need to remove tag on this upload, the updated version is not up-to-spec
+ tagFallback.interactable = false;
+ tagFallback.GetComponentInChildren<Text>().text = "Use for Fallback (avatar not valid, tag will be cleared)";
+ break;
+ }
+
+ blueprintDescription.text = apiAvatar.description;
+ shouldUpdateImageToggle.interactable = true;
+ shouldUpdateImageToggle.isOn = false;
+ liveBpImage.enabled = false;
+ bpImage.enabled = true;
+
+ ImageDownloader.DownloadImage(apiAvatar.imageUrl, 0, (Texture2D obj) => bpImage.texture = obj, null);
+ }
+ else // user does not own apiAvatar id associated with descriptor
+ {
+ Debug.LogErrorFormat("{0} is not an owner of {1}", apiAvatar.authorId, pipelineManager.user.id);
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(true);
+ }
+ }
+ else
+ {
+ titleText.text = "New Avatar";
+ shouldUpdateImageToggle.interactable = false;
+ shouldUpdateImageToggle.isOn = true;
+ liveBpImage.enabled = true;
+ bpImage.enabled = false;
+ tagFallback.isOn = false;
+
+ // Janky fix for an avatar's blueprint image not showing up the very first time you press publish in a project until you resize the window
+ // can remove if we fix the underlying issue or move publishing out of Play Mode
+ string firstTimeResize = $"{Application.identifier}-firstTimeResize";
+ if (!PlayerPrefs.HasKey(firstTimeResize))
+ {
+ GameViewMethods.ResizeGameView();
+ PlayerPrefs.SetInt(firstTimeResize, 1);
+ }
+
+ tagFallback.transform.parent.gameObject.SetActive(true);
+ switch (pipelineManager.fallbackStatus)
+ {
+ case PipelineManager.FallbackStatus.Valid:
+#if UNITY_ANDROID
+ tagFallback.interactable = true;
+ tagFallback.GetComponentInChildren<Text>().text = "Use for Fallback";
+#else
+ tagFallback.interactable = false;
+ tagFallback.GetComponentInChildren<Text>().text = "Use for Fallback (change only with Android upload)";
+#endif
+ break;
+ case PipelineManager.FallbackStatus.InvalidPerformance:
+ case PipelineManager.FallbackStatus.InvalidRig:
+ tagFallback.transform.parent.gameObject.SetActive(true);
+ tagFallback.interactable = false;
+ tagFallback.GetComponentInChildren<Text>().text = "Use for Fallback (avatar not valid, tag will be cleared)";
+ break;
+ }
+ }
+ }
+ else
+ {
+ waitingPanel.SetActive(true);
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(false);
+ }
+
+ if (APIUser.CurrentUser != null && APIUser.CurrentUser.hasSuperPowers)
+ developerAvatar.gameObject.SetActive(true);
+ else
+ developerAvatar.gameObject.SetActive(false);
+ }
+
+ public void SetupUpload()
+ {
+ uploadTitle = "Preparing For Upload";
+ isUploading = true;
+
+ string abPath = UnityEditor.EditorPrefs.GetString("currentBuildingAssetBundlePath");
+
+ string unityPackagePath = UnityEditor.EditorPrefs.GetString("VRC_exportedUnityPackagePath");
+
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_scene_changed", true);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_sex", contentSex.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_violence", contentViolence.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_gore", contentGore.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_other", contentOther.isOn);
+
+ if (string.IsNullOrEmpty(apiAvatar.id))
+ {
+ pipelineManager.AssignId();
+ apiAvatar.id = pipelineManager.blueprintId;
+ }
+
+ string avatarId = apiAvatar.id;
+ int version = isUpdate ? apiAvatar.version + 1 : 1;
+ PrepareVRCPathForS3(abPath, avatarId, version, ApiAvatar.VERSION);
+
+ if (!string.IsNullOrEmpty(unityPackagePath) && System.IO.File.Exists(unityPackagePath))
+ {
+ VRC.Core.Logger.Log("Found unity package path. Preparing to upload!", DebugLevel.All);
+ PrepareUnityPackageForS3(unityPackagePath, avatarId, version, ApiAvatar.VERSION);
+ }
+
+ StartCoroutine(UploadNew());
+ }
+
+ IEnumerator UploadNew()
+ {
+ bool caughtInvalidInput = false;
+ if (!ValidateNameInput(blueprintName))
+ caughtInvalidInput = true;
+
+ if (caughtInvalidInput)
+ yield break;
+
+ VRC.Core.Logger.Log("Starting upload", DebugLevel.Always);
+
+ // upload unity package
+ if (!string.IsNullOrEmpty(uploadUnityPackagePath))
+ {
+ yield return StartCoroutine(UploadFile(uploadUnityPackagePath, isUpdate ? apiAvatar.unityPackageUrl : "", GetFriendlyAvatarFileName("Unity package"), "Unity package",
+ delegate (string fileUrl)
+ {
+ cloudFrontUnityPackageUrl = fileUrl;
+ }
+ ));
+ }
+
+ // upload asset bundle
+ if (!string.IsNullOrEmpty(uploadVrcPath))
+ {
+ yield return StartCoroutine(UploadFile(uploadVrcPath, isUpdate ? apiAvatar.assetUrl : "", GetFriendlyAvatarFileName("Asset bundle"), "Asset bundle",
+ delegate (string fileUrl)
+ {
+ cloudFrontAssetUrl = fileUrl;
+ }
+ ));
+ }
+
+ if (isUpdate)
+ yield return StartCoroutine(UpdateBlueprint());
+ else
+ yield return StartCoroutine(CreateBlueprint());
+
+ OnSDKPipelineComplete();
+ }
+
+ private string GetFriendlyAvatarFileName(string type)
+ {
+ return "Avatar - " + blueprintName.text + " - " + type + " - " + Application.unityVersion + "_" + ApiWorld.VERSION.ApiVersion +
+ "_" + VRC.Tools.Platform + "_" + API.GetServerEnvironmentForApiUrl();
+ }
+
+ List<string> BuildTags()
+ {
+ var tags = new List<string>();
+ if (contentSex.isOn)
+ tags.Add("content_sex");
+ if (contentViolence.isOn)
+ tags.Add("content_violence");
+ if (contentGore.isOn)
+ tags.Add("content_gore");
+ if (contentOther.isOn)
+ tags.Add("content_other");
+
+ if (APIUser.CurrentUser.hasSuperPowers)
+ {
+ if (developerAvatar.isOn)
+ tags.Add("developer");
+ }
+
+ if (tagFallback.isOn)
+ tags.Add("author_quest_fallback");
+
+ return tags;
+ }
+
+ protected override IEnumerator CreateBlueprint()
+ {
+ yield return StartCoroutine(UpdateImage(isUpdate ? apiAvatar.imageUrl : "", GetFriendlyAvatarFileName("Image")));
+
+ ApiAvatar avatar = new ApiAvatar
+ {
+ id = pipelineManager.blueprintId,
+ authorName = pipelineManager.user.displayName,
+ authorId = pipelineManager.user.id,
+ name = blueprintName.text,
+ imageUrl = cloudFrontImageUrl,
+ assetUrl = cloudFrontAssetUrl,
+ description = blueprintDescription.text,
+ tags = BuildTags(),
+ unityPackageUrl = cloudFrontUnityPackageUrl,
+ releaseStatus = sharePublic.isOn ? "public" : "private"
+ };
+
+ bool doneUploading = false;
+ bool wasError = false;
+
+ avatar.Post(
+ (c) =>
+ {
+ ApiAvatar savedBP = (ApiAvatar)c.Model;
+ pipelineManager.blueprintId = savedBP.id;
+ UnityEditor.EditorPrefs.SetString("blueprintID-" + pipelineManager.GetInstanceID().ToString(), savedBP.id);
+
+ AnalyticsSDK.AvatarUploaded(savedBP, false);
+ doneUploading = true;
+ },
+ (c) =>
+ {
+ Debug.LogError(c.Error);
+ SetUploadProgress("Saving Avatar", "Error saving blueprint.", 0.0f);
+ doneUploading = true;
+ wasError = true;
+ });
+
+ while (!doneUploading)
+ yield return null;
+
+ if (wasError)
+ yield return new WaitUntil(() => UnityEditor.EditorUtility.DisplayDialog("VRChat SDK", "Error saving blueprint.", "Okay"));
+ }
+
+ protected override IEnumerator UpdateBlueprint()
+ {
+ bool doneUploading = false;
+
+ apiAvatar.name = blueprintName.text;
+ apiAvatar.description = blueprintDescription.text;
+ apiAvatar.assetUrl = cloudFrontAssetUrl;
+ apiAvatar.releaseStatus = sharePublic.isOn ? "public" : "private";
+ apiAvatar.tags = BuildTags();
+ apiAvatar.unityPackageUrl = cloudFrontUnityPackageUrl;
+
+ if (shouldUpdateImageToggle.isOn)
+ {
+ yield return StartCoroutine(UpdateImage(isUpdate ? apiAvatar.imageUrl : "", GetFriendlyAvatarFileName("Image")));
+ apiAvatar.imageUrl = cloudFrontImageUrl;
+ }
+
+ SetUploadProgress("Saving Avatar", "Almost finished!!", 0.8f);
+ apiAvatar.Save(
+ (c) => { AnalyticsSDK.AvatarUploaded(apiAvatar, true); doneUploading = true; },
+ (c) => {
+ Debug.LogError(c.Error);
+ SetUploadProgress("Saving Avatar", "Error saving blueprint.", 0.0f);
+ doneUploading = true;
+ });
+
+ while (!doneUploading)
+ yield return null;
+ }
+
+ void ToggleUpdateImage(bool isOn)
+ {
+ if (isOn)
+ {
+ bpImage.enabled = false;
+ liveBpImage.enabled = true;
+ }
+ else
+ {
+ bpImage.enabled = true;
+ liveBpImage.enabled = false;
+ ImageDownloader.DownloadImage(apiAvatar.imageUrl, 0, obj => bpImage.texture = obj, null);
+ }
+ }
+
+ void OnDestroy()
+ {
+ UnityEditor.EditorUtility.ClearProgressBar();
+ UnityEditor.EditorPrefs.DeleteKey("currentBuildingAssetBundlePath");
+ }
+ }
+#endif
+}
+
+
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs.meta
new file mode 100644
index 00000000..cf873f35
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeBlueprintCreation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1e5ebf65c5dceeb4c909aa7812bd2999
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs
new file mode 100644
index 00000000..e002d2f8
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs
@@ -0,0 +1,681 @@
+#define COMMUNITY_LABS_SDK
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.SceneManagement;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Resources;
+using VRC.Core;
+using System;
+using System.IO;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace VRCSDK2
+{
+#if UNITY_EDITOR
+ public class RuntimeWorldCreation : RuntimeAPICreation
+ {
+ public GameObject waitingPanel;
+ public GameObject blueprintPanel;
+ public GameObject errorPanel;
+
+ public Text titleText;
+ public InputField blueprintName;
+ public InputField blueprintDescription;
+ public InputField worldCapacity;
+ public RawImage bpImage;
+ public Image liveBpImage;
+ public Toggle shouldUpdateImageToggle;
+ public Toggle releasePublic;
+ public Toggle contentNsfw;
+
+ public Toggle contentSex;
+ public Toggle contentViolence;
+ public Toggle contentGore;
+ public Toggle contentOther;
+
+ public Toggle contentFeatured;
+ public Toggle contentSDKExample;
+
+ public Image showInWorldsMenuGroup;
+ public Toggle showInActiveWorlds;
+ public Toggle showInPopularWorlds;
+ public Toggle showInNewWorlds;
+
+ public InputField userTags;
+
+ public UnityEngine.UI.Button uploadButton;
+
+ public UnityEngine.UI.Button openCommunityLabsDocsButton;
+
+ public GameObject publishToCommunityLabsPanel;
+
+ private Toggle publishToCommLabsToggle;
+
+ private ApiWorld worldRecord;
+
+ private const int MAX_USER_TAGS_FOR_WORLD = 5;
+ private const int MAX_CHARACTERS_ALLOWED_IN_USER_TAG = 20;
+ List<String> customTags;
+
+ public static bool IsCurrentWorldInCommunityLabs = false;
+ public static bool IsCurrentWorldUploaded = false;
+ public static bool IsCurrentWorldPubliclyPublished = false;
+ public static bool HasExceededPublishLimit = false;
+
+ new void Start()
+ {
+ if (!Application.isEditor || !Application.isPlaying)
+ return;
+
+ base.Start();
+
+ IsCurrentWorldInCommunityLabs = false;
+ IsCurrentWorldUploaded = false;
+ IsCurrentWorldPubliclyPublished = false;
+
+
+ var desc = pipelineManager.GetComponent<VRC.SDKBase.VRC_SceneDescriptor>();
+ desc.PositionPortraitCamera(imageCapture.shotCamera.transform);
+
+ Application.runInBackground = true;
+ UnityEngine.XR.XRSettings.enabled = false;
+
+ uploadButton.onClick.AddListener(SetupUpload);
+
+ openCommunityLabsDocsButton.onClick.AddListener(OpenCommunityLabsDocumentation);
+
+ shouldUpdateImageToggle.onValueChanged.AddListener(ToggleUpdateImage);
+
+ releasePublic.gameObject.SetActive(false);
+
+ System.Action<string> onError = (err) => {
+ VRC.Core.Logger.LogError("Could not authenticate - " + err, DebugLevel.Always);
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(true);
+ };
+
+ if (!ApiCredentials.Load())
+ onError("Not logged in");
+ else
+ APIUser.InitialFetchCurrentUser(
+ delegate (ApiModelContainer<APIUser> c)
+ {
+ UserLoggedInCallback(c.Model as APIUser);
+ },
+ delegate (ApiModelContainer<APIUser> c)
+ {
+ onError(c.Error);
+ }
+ );
+
+#if !COMMUNITY_LABS_SDK
+ publishToCommunityLabsPanel.gameObject.SetActive(false);
+#endif
+ }
+
+ void UserLoggedInCallback(APIUser user)
+ {
+ pipelineManager.user = user;
+
+ ApiWorld model = new ApiWorld();
+ model.id = pipelineManager.blueprintId;
+ model.Fetch(null,
+ (c) =>
+ {
+ VRC.Core.Logger.Log("<color=magenta>Updating an existing world.</color>", DebugLevel.All);
+ worldRecord = c.Model as ApiWorld;
+ pipelineManager.completedSDKPipeline = !string.IsNullOrEmpty(worldRecord.authorId);
+ GetUserUploadInformationAndSetupUI(model.id);
+ },
+ (c) =>
+ {
+ VRC.Core.Logger.Log("<color=magenta>World record not found, creating a new world.</color>", DebugLevel.All);
+ worldRecord = new ApiWorld { capacity = 16 };
+ pipelineManager.completedSDKPipeline = false;
+ worldRecord.id = pipelineManager.blueprintId;
+ GetUserUploadInformationAndSetupUI(model.id);
+ });
+ }
+
+ void CheckWorldStatus(string worldId, Action onCheckComplete)
+ {
+ // check if world has been previously uploaded, and if world is in community labs
+ ApiWorld.FetchUploadedWorlds(
+ delegate (IEnumerable<ApiWorld> worlds)
+ {
+ ApiWorld selectedWorld = worlds.FirstOrDefault(w => w.id == worldId);
+ if (null!=selectedWorld)
+ {
+ IsCurrentWorldInCommunityLabs = selectedWorld.IsCommunityLabsWorld;
+ IsCurrentWorldPubliclyPublished = selectedWorld.IsPublicPublishedWorld;
+ IsCurrentWorldUploaded = true;
+ }
+ if (onCheckComplete != null) onCheckComplete();
+
+ },
+ delegate (string err)
+ {
+ IsCurrentWorldInCommunityLabs = false;
+ IsCurrentWorldUploaded = false;
+ IsCurrentWorldPubliclyPublished = false;
+ Debug.Log("CheckWorldStatus error:" + err);
+ if (onCheckComplete != null) onCheckComplete();
+ }
+ );
+ }
+
+
+ void GetUserUploadInformationAndSetupUI(string worldId)
+ {
+ CheckWorldStatus(worldId, delegate()
+ {
+ bool hasSufficientTrustLevelToPublishToCommunityLabs = APIUser.CurrentUser.hasKnownTrustLevel;
+ APIUser.FetchPublishWorldsInformation(
+ (c) =>
+ {
+ try
+ {
+ Dictionary<string, object> publish = c as Dictionary<string, object>;
+ if (publish["canPublish"] is bool)
+ {
+ HasExceededPublishLimit = !(bool)(publish["canPublish"]);
+ }
+ else
+ HasExceededPublishLimit = true;
+ }
+ catch (Exception)
+ {
+ HasExceededPublishLimit = true;
+ }
+
+ if(Application.isPlaying)
+ {
+ SetupUI(hasSufficientTrustLevelToPublishToCommunityLabs, HasExceededPublishLimit);
+ }
+ },
+ (c) =>
+ {
+ if(Application.isPlaying)
+ {
+ SetupUI(hasSufficientTrustLevelToPublishToCommunityLabs, HasExceededPublishLimit);
+ }
+ }
+ );
+ }
+ );
+ }
+
+ void SetupUI(bool hasEnoughTrustToPublishToCL = false, bool hasExceededWeeklyPublishLimit = false)
+ {
+#if COMMUNITY_LABS_SDK
+ // do not display community labs panel if updating an existing CL world or updating a public world
+ publishToCommunityLabsPanel.gameObject.SetActive(!IsCurrentWorldUploaded);
+#endif
+
+ if (!ValidateAssetBundleBlueprintID(worldRecord.id))
+ {
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(true);
+ OnSDKPipelineError("The asset bundle is out of date. Please rebuild the scene using 'New Build'.", "The blueprint ID in the scene does not match the id in the asset bundle.");
+ return;
+ }
+
+ contentFeatured.gameObject.SetActive(APIUser.CurrentUser.hasSuperPowers);
+ contentSDKExample.gameObject.SetActive(APIUser.CurrentUser.hasSuperPowers);
+
+ if (APIUser.Exists(pipelineManager.user))
+ {
+ waitingPanel.SetActive(false);
+ blueprintPanel.SetActive(true);
+ errorPanel.SetActive(false);
+
+ if (string.IsNullOrEmpty(worldRecord.authorId) || worldRecord.authorId == pipelineManager.user.id)
+ {
+ titleText.text = "Configure World";
+ blueprintName.text = worldRecord.name;
+ worldCapacity.text = worldRecord.capacity.ToString();
+ contentSex.isOn = worldRecord.tags.Contains("content_sex");
+ contentViolence.isOn = worldRecord.tags.Contains("content_violence");
+ contentGore.isOn = worldRecord.tags.Contains("content_gore");
+ contentOther.isOn = worldRecord.tags.Contains("content_other");
+ shouldUpdateImageToggle.interactable = isUpdate;
+ shouldUpdateImageToggle.isOn = !isUpdate;
+ liveBpImage.enabled = !isUpdate;
+ bpImage.enabled = isUpdate;
+
+ if (!APIUser.CurrentUser.hasSuperPowers)
+ {
+ releasePublic.gameObject.SetActive(false);
+ releasePublic.isOn = false;
+ releasePublic.interactable = false;
+
+ contentFeatured.isOn = contentSDKExample.isOn = false;
+ }
+ else
+ {
+ contentFeatured.isOn = worldRecord.tags.Contains("content_featured");
+ contentSDKExample.isOn = worldRecord.tags.Contains("content_sdk_example");
+
+ releasePublic.isOn = worldRecord.releaseStatus == "public";
+ releasePublic.gameObject.SetActive(true);
+ }
+
+ // "show in worlds menu"
+ if (APIUser.CurrentUser.hasSuperPowers)
+ {
+ showInWorldsMenuGroup.gameObject.SetActive(true);
+ showInActiveWorlds.isOn = !worldRecord.tags.Contains("admin_hide_active");
+ showInPopularWorlds.isOn = !worldRecord.tags.Contains("admin_hide_popular");
+ showInNewWorlds.isOn = !worldRecord.tags.Contains("admin_hide_new");
+ }
+ else
+ {
+ showInWorldsMenuGroup.gameObject.SetActive(false);
+ }
+
+ blueprintDescription.text = worldRecord.description;
+
+ userTags.text = "";
+ foreach (var tag in worldRecord.publicTags)
+ {
+ userTags.text = userTags.text + tag.Replace("author_tag_", "");
+ userTags.text = userTags.text + " ";
+ }
+
+ ImageDownloader.DownloadImage(worldRecord.imageUrl, 0, obj => bpImage.texture = obj, null);
+ }
+ else // user does not own world id associated with descriptor
+ {
+ Debug.LogErrorFormat("{0} is not an owner of {1}", worldRecord.authorId, pipelineManager.user.id);
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(true);
+ }
+ }
+ else
+ {
+ waitingPanel.SetActive(true);
+ blueprintPanel.SetActive(false);
+ errorPanel.SetActive(false);
+
+ if (!APIUser.CurrentUser.hasSuperPowers)
+ {
+ releasePublic.gameObject.SetActive(false);
+ releasePublic.isOn = false;
+ releasePublic.interactable = false;
+ }
+ else
+ {
+ releasePublic.gameObject.SetActive(true);
+ releasePublic.isOn = false;
+ }
+ }
+
+ // set up publish to Community Labs checkbox and text
+ int worldsPublishedThisWeek = hasExceededWeeklyPublishLimit ? 1 : 0;
+ int maximumWorldsAllowedToPublishPerWeek = 1;
+ publishToCommLabsToggle = publishToCommunityLabsPanel.GetComponentInChildren<Toggle>();
+
+ if (null != publishToCommLabsToggle)
+ {
+ // disable publishing to CL checkbox if not enough trust or exceeded publish limit
+ publishToCommLabsToggle.interactable = hasEnoughTrustToPublishToCL && !hasExceededWeeklyPublishLimit;
+
+ Text publishText = publishToCommLabsToggle.gameObject.GetComponentInChildren<Text>();
+ if (null != publishText)
+ {
+ if (!hasEnoughTrustToPublishToCL)
+ {
+ publishText.text = "Not enough Trust to Publish to Community Labs";
+ }
+ else
+ {
+ if (hasExceededWeeklyPublishLimit)
+ {
+ publishText.text = "Publish limit for Community Labs Exceeded\n" + "(" + worldsPublishedThisWeek + "/" + maximumWorldsAllowedToPublishPerWeek + " Published this week)";
+ }
+ else
+ {
+ publishText.text = "Publish to Community Labs\n" + "(" + worldsPublishedThisWeek + "/" + maximumWorldsAllowedToPublishPerWeek + " Published this week)";
+ }
+ }
+ }
+ }
+ }
+
+ public void SetupUpload()
+ {
+ if (!ParseUserTags())
+ return;
+
+ publishingToCommunityLabs = (publishToCommLabsToggle != null) && (publishToCommLabsToggle.isActiveAndEnabled) && (publishToCommLabsToggle.isOn);
+
+ uploadTitle = "Preparing For Upload";
+ isUploading = true;
+
+ string abPath = UnityEditor.EditorPrefs.GetString("currentBuildingAssetBundlePath");
+
+
+ string unityPackagePath = UnityEditor.EditorPrefs.GetString("VRC_exportedUnityPackagePath");
+
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_scene_changed", true);
+ UnityEditor.EditorPrefs.SetInt("VRCSDK2_capacity", System.Convert.ToInt16(worldCapacity.text));
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_sex", contentSex.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_violence", contentViolence.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_gore", contentGore.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_other", contentOther.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_release_public", releasePublic.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_featured", contentFeatured.isOn);
+ UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_sdk_example", contentSDKExample.isOn);
+
+ if (string.IsNullOrEmpty(worldRecord.id))
+ {
+ pipelineManager.AssignId();
+ worldRecord.id = pipelineManager.blueprintId;
+ }
+
+ string blueprintId = worldRecord.id;
+ int version = Mathf.Max(1, worldRecord.version + 1);
+ PrepareVRCPathForS3(abPath, blueprintId, version, ApiWorld.VERSION);
+
+ if (!string.IsNullOrEmpty(unityPackagePath) && System.IO.File.Exists(unityPackagePath))
+ {
+ VRC.Core.Logger.Log("Found unity package path. Preparing to upload!", DebugLevel.All);
+ PrepareUnityPackageForS3(unityPackagePath, blueprintId, version, ApiWorld.VERSION);
+ }
+
+ StartCoroutine(UploadNew());
+ }
+
+ void OnUploadedWorld()
+ {
+ const string devUrl = "https://dev-api.vrchat.cloud";
+ const string releaseUrl = "https://vrchat.com";
+
+ string uploadedWorldURL = (API.IsDevApi() ? devUrl : releaseUrl) + "/home/world/" + pipelineManager.blueprintId;
+ OnSDKPipelineComplete(uploadedWorldURL);
+ }
+
+ IEnumerator UploadNew()
+ {
+ bool caughtInvalidInput = false;
+ if (!ValidateNameInput(blueprintName))
+ caughtInvalidInput = true;
+
+ if (caughtInvalidInput)
+ yield break;
+
+ VRC.Core.Logger.Log("Starting upload", DebugLevel.Always);
+
+ // upload unity package
+ if (!string.IsNullOrEmpty(uploadUnityPackagePath))
+ {
+ yield return StartCoroutine(UploadFile(uploadUnityPackagePath, isUpdate ? worldRecord.unityPackageUrl : "", GetFriendlyWorldFileName("Unity package"), "Unity package",
+ delegate (string fileUrl)
+ {
+ cloudFrontUnityPackageUrl = fileUrl;
+ }
+ ));
+ }
+
+ // upload asset bundle
+ if (!string.IsNullOrEmpty(uploadVrcPath))
+ {
+ yield return StartCoroutine(UploadFile(uploadVrcPath, isUpdate ? worldRecord.assetUrl : "", GetFriendlyWorldFileName("Asset bundle"), "Asset bundle",
+ delegate (string fileUrl)
+ {
+ cloudFrontAssetUrl = fileUrl;
+ }
+ ));
+ }
+
+ if (isUpdate)
+ yield return StartCoroutine(UpdateBlueprint());
+ else
+ yield return StartCoroutine(CreateBlueprint());
+
+ if (publishingToCommunityLabs)
+ {
+ ApiWorld.PublishWorldToCommunityLabs(pipelineManager.blueprintId,
+ (world) => OnUploadedWorld(),
+ (err) =>
+ {
+ Debug.LogError("PublishWorldToCommunityLabs error:" + err);
+ OnUploadedWorld();
+ }
+ );
+ }
+ else
+ {
+ OnUploadedWorld();
+ }
+ }
+
+ private string GetFriendlyWorldFileName(string type)
+ {
+ return "World - " + blueprintName.text + " - " + type + " - " + Application.unityVersion + "_" + ApiWorld.VERSION.ApiVersion +
+ "_" + VRC.Tools.Platform + "_" + API.GetServerEnvironmentForApiUrl();
+ }
+
+ List<string> BuildTags()
+ {
+ var tags = new List<string>();
+ if (contentSex.isOn)
+ tags.Add("content_sex");
+ if (contentViolence.isOn)
+ tags.Add("content_violence");
+ if (contentGore.isOn)
+ tags.Add("content_gore");
+ if (contentOther.isOn)
+ tags.Add("content_other");
+
+ if (APIUser.CurrentUser.hasSuperPowers)
+ {
+ if (contentFeatured.isOn)
+ tags.Add("content_featured");
+ if (contentSDKExample.isOn)
+ tags.Add("content_sdk_example");
+ if(releasePublic.isOn)
+ tags.Add("admin_approved");
+ }
+
+ // "show in worlds menu"
+ if (APIUser.CurrentUser.hasSuperPowers)
+ {
+ if (!showInActiveWorlds.isOn)
+ tags.Add("admin_hide_active");
+ if (!showInPopularWorlds.isOn)
+ tags.Add("admin_hide_popular");
+ if (!showInNewWorlds.isOn)
+ tags.Add("admin_hide_new");
+ }
+
+ // add any author tags
+ foreach (var word in customTags)
+ {
+ // add all custom tags with "author_tag_" prefix
+ tags.Add("author_tag_" + word);
+ }
+
+ return tags;
+ }
+
+ bool ParseUserTags()
+ {
+ bool validTags = true;
+ customTags = new List<string>();
+ char[] delimiterChars = { ' ', ',', '.', ':', '\t', '\n', '"', '#' };
+
+ // split user tags into individual words
+ string[] words = userTags.text.Split(delimiterChars, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var word in words)
+ {
+ customTags.Add(word.ToLower());
+ }
+
+ // check that number of tags is within tag limit
+ if (words.Count() > MAX_USER_TAGS_FOR_WORLD)
+ {
+ validTags = false;
+ UnityEditor.EditorUtility.DisplayDialog("Tags are limited to a maximum of " + MAX_USER_TAGS_FOR_WORLD + " per world.", "Please remove excess tags before uploading!", "OK");
+ }
+ else
+ {
+ // check that no tags exceed maximum tag length
+ int maximumTagLength = 0;
+ foreach (string item in words)
+ {
+ if (item.Length > maximumTagLength)
+ {
+ maximumTagLength = item.Length;
+ }
+ }
+
+ if (maximumTagLength > MAX_CHARACTERS_ALLOWED_IN_USER_TAG)
+ {
+ validTags = false;
+ UnityEditor.EditorUtility.DisplayDialog("Tags are limited to a maximum of " + MAX_CHARACTERS_ALLOWED_IN_USER_TAG + " characters per tag.", "One or more of your tags exceeds the maximum " + MAX_CHARACTERS_ALLOWED_IN_USER_TAG + " character limit.\n\n" + "Please shorten tags before uploading!", "OK");
+ }
+ else
+ {
+ // make sure tags are all alphanumeric
+ foreach (var word in words)
+ {
+ if (!word.All(char.IsLetterOrDigit))
+ {
+ validTags = false;
+ UnityEditor.EditorUtility.DisplayDialog("Tags should consist of alphanumeric characters only.", "Please remove any non-alphanumeric characters from tags before uploading!", "OK");
+ }
+ }
+ }
+ }
+
+ return validTags;
+ }
+
+ protected override IEnumerator CreateBlueprint()
+ {
+ yield return StartCoroutine(UpdateImage(isUpdate ? worldRecord.imageUrl : "", GetFriendlyWorldFileName("Image")));
+
+ SetUploadProgress("Saving Blueprint to user", "Almost finished!!", 0.0f);
+ ApiWorld world = new ApiWorld
+ {
+ id = worldRecord.id,
+ authorName = pipelineManager.user.displayName,
+ authorId = pipelineManager.user.id,
+ name = blueprintName.text,
+ imageUrl = cloudFrontImageUrl,
+ assetUrl = cloudFrontAssetUrl,
+ unityPackageUrl = cloudFrontUnityPackageUrl,
+ description = blueprintDescription.text,
+ tags = BuildTags(),
+ releaseStatus = (releasePublic.isOn) ? ("public") : ("private"),
+ capacity = System.Convert.ToInt16(worldCapacity.text),
+ occupants = 0,
+ shouldAddToAuthor = true
+ };
+
+ if (APIUser.CurrentUser.hasSuperPowers)
+ world.isCurated = contentFeatured.isOn || contentSDKExample.isOn;
+ else
+ world.isCurated = false;
+
+ bool doneUploading = false;
+ world.Post(
+ (c) =>
+ {
+ ApiWorld savedBP = (ApiWorld)c.Model;
+ pipelineManager.blueprintId = savedBP.id;
+ UnityEditor.EditorPrefs.SetString("blueprintID-" + pipelineManager.GetInstanceID().ToString(), savedBP.id);
+ VRC.Core.Logger.Log("Setting blueprintID on pipeline manager and editor prefs", DebugLevel.All);
+ doneUploading = true;
+ },
+ (c) => { doneUploading = true; Debug.LogError(c.Error); });
+
+ while (!doneUploading)
+ yield return null;
+ }
+
+ protected override IEnumerator UpdateBlueprint()
+ {
+ bool doneUploading = false;
+
+ worldRecord.name = blueprintName.text;
+ worldRecord.description = blueprintDescription.text;
+ worldRecord.capacity = System.Convert.ToInt16(worldCapacity.text);
+ worldRecord.assetUrl = cloudFrontAssetUrl;
+ worldRecord.tags = BuildTags();
+ worldRecord.releaseStatus = (releasePublic.isOn) ? ("public") : ("private");
+ worldRecord.unityPackageUrl = cloudFrontUnityPackageUrl;
+ worldRecord.isCurated = contentFeatured.isOn || contentSDKExample.isOn;
+
+ if (shouldUpdateImageToggle.isOn)
+ {
+ yield return StartCoroutine(UpdateImage(isUpdate ? worldRecord.imageUrl : "", GetFriendlyWorldFileName("Image")));
+
+ worldRecord.imageUrl = cloudFrontImageUrl;
+ }
+
+ SetUploadProgress("Saving Blueprint", "Almost finished!!", 0.0f);
+ worldRecord.Save((c) => doneUploading = true, (c) => { doneUploading = true; Debug.LogError(c.Error); });
+
+ while (!doneUploading)
+ yield return null;
+ }
+
+ void ToggleUpdateImage(bool isOn)
+ {
+ if (isOn)
+ {
+ bpImage.enabled = false;
+ liveBpImage.enabled = true;
+ }
+ else
+ {
+ bpImage.enabled = true;
+ liveBpImage.enabled = false;
+ ImageDownloader.DownloadImage(worldRecord.imageUrl, 0, obj => bpImage.texture = obj, null);
+ }
+ }
+
+ protected override void DisplayUpdateCompletedDialog(string contentUrl = null)
+ {
+#if UNITY_EDITOR
+#if COMMUNITY_LABS_SDK
+ if (null != contentUrl)
+ {
+ CheckWorldStatus(pipelineManager.blueprintId, delegate ()
+ {
+ ContentUploadedDialog window = (ContentUploadedDialog)EditorWindow.GetWindow(typeof(ContentUploadedDialog), true, "VRCSDK", true);
+ window.setContentURL(contentUrl);
+ window.Show();
+ // refresh UI based on uploaded world
+ GetUserUploadInformationAndSetupUI(pipelineManager.blueprintId);
+ }
+ );
+ }
+ else
+#endif
+ base.DisplayUpdateCompletedDialog(contentUrl);
+#endif
+ }
+
+ private void OpenCommunityLabsDocumentation()
+ {
+ Application.OpenURL(CommunityLabsConstants.COMMUNITY_LABS_DOCUMENTATION_URL);
+ }
+
+ void OnDestroy()
+ {
+ UnityEditor.EditorUtility.ClearProgressBar();
+ UnityEditor.EditorPrefs.DeleteKey("currentBuildingAssetBundlePath");
+ }
+ }
+#endif
+}
+
+
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs.meta
new file mode 100644
index 00000000..8121e7c2
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/RuntimeWorldCreation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2bd5ee5d69ee0f3449bf2f81fcb7f4e3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs
new file mode 100644
index 00000000..86d56a5d
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs
@@ -0,0 +1,18 @@
+using System.Collections;
+using UnityEngine;
+
+public class SceneSaver
+{
+ static public void SaveScene()
+ {
+#if UNITY_EDITOR
+ var activeScene = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene();
+ UnityEditor.SceneManagement.EditorSceneManager.SaveScene(activeScene);
+
+ UnityEditor.EditorApplication.isPaused = false;
+ UnityEditor.EditorApplication.isPlaying = false;
+
+ UnityEditor.SceneManagement.EditorSceneManager.OpenScene(activeScene.name);
+#endif
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs.meta
new file mode 100644
index 00000000..454a9ab4
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/SceneSaver.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 0d49300ad532d4ae6b569b28de5b7dac
+timeCreated: 1446917779
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation.meta
new file mode 100644
index 00000000..c172df80
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4d5dd0c5d2dfe12409684776a50ce130
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs
new file mode 100644
index 00000000..39dd14a7
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs
@@ -0,0 +1 @@
+/* Migration File: Intentionally Left Blank */ \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs.meta
new file mode 100644
index 00000000..f6e03bb1
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/AvatarValidation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d497915ac8463e048aeb2c934a36c299
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance.meta
new file mode 100644
index 00000000..07fd6f1c
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 16b48f3cc7015584c9d4157931f1c673
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs
new file mode 100644
index 00000000..3269bc89
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs
@@ -0,0 +1,165 @@
+using System.Collections;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance
+{
+ public static class AvatarPerformance
+ {
+ #region Public Constants
+
+ public const int DEFAULT_DYNAMIC_BONE_MAX_SIMULATED_BONE_LIMIT = 32;
+ public const int DEFAULT_DYNAMIC_BONE_MAX_COLLIDER_CHECK_LIMIT = 8;
+
+ #if UNITY_ANDROID || UNITY_IOS
+ internal const PerformanceRating AVATAR_PERFORMANCE_RATING_MINIMUM_TO_DISPLAY_DEFAULT = PerformanceRating.Medium;
+ internal const PerformanceRating AVATAR_PERFORMANCE_RATING_MINIMUM_TO_DISPLAY_MIN = PerformanceRating.Medium;
+ internal const PerformanceRating AVATAR_PERFORMANCE_RATING_MINIMUM_TO_DISPLAY_MAX = PerformanceRating.Poor;
+ #else
+ internal const PerformanceRating AVATAR_PERFORMANCE_RATING_MINIMUM_TO_DISPLAY_DEFAULT = PerformanceRating.VeryPoor;
+ internal const PerformanceRating AVATAR_PERFORMANCE_RATING_MINIMUM_TO_DISPLAY_MIN = PerformanceRating.Medium;
+ internal const PerformanceRating AVATAR_PERFORMANCE_RATING_MINIMUM_TO_DISPLAY_MAX = PerformanceRating.VeryPoor;
+ #endif
+
+ #endregion
+
+ #region Public Delegates
+
+ public delegate bool IgnoreDelegate(Component component);
+
+ public delegate void FilterBlockCallback();
+
+ public static IgnoreDelegate ShouldIgnoreComponent { get; set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void CalculatePerformanceStats(string avatarName, GameObject avatarObject, AvatarPerformanceStats perfStats)
+ {
+ perfStats.Reset();
+ perfStats.avatarName = avatarName;
+
+ PerformanceScannerSet performanceScannerSet = GetPerformanceScannerSet();
+ if(performanceScannerSet != null)
+ {
+ performanceScannerSet.RunPerformanceScan(avatarObject, perfStats, ShouldIgnoreComponentInternal);
+ }
+
+ // cache performance ratings
+ perfStats.CalculateAllPerformanceRatings();
+ }
+
+ public static IEnumerator CalculatePerformanceStatsEnumerator(string avatarName, GameObject avatarObject, AvatarPerformanceStats perfStats)
+ {
+ perfStats.Reset();
+ perfStats.avatarName = avatarName;
+
+ PerformanceScannerSet performanceScannerSet = GetPerformanceScannerSet();
+ if(performanceScannerSet != null)
+ {
+ yield return performanceScannerSet.RunPerformanceScanEnumerator(avatarObject, perfStats, ShouldIgnoreComponentInternal);
+ }
+
+ // cache performance ratings
+ perfStats.CalculateAllPerformanceRatings();
+ }
+
+ public static IEnumerator ApplyPerformanceFiltersEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, PerformanceRating minPerfRating, FilterBlockCallback onBlock)
+ {
+ // Performance Filtering is disabled.
+ if(minPerfRating == PerformanceRating.None)
+ {
+ yield break;
+ }
+
+ PerformanceFilterSet performanceFilterSet = GetPerformanceFilterSet();
+ if(performanceFilterSet == null)
+ {
+ yield break;
+ }
+
+ bool avatarBlocked = false;
+ yield return performanceFilterSet.ApplyPerformanceFilters(
+ avatarObject,
+ perfStats,
+ minPerfRating,
+ ShouldIgnoreComponentInternal,
+ () => { avatarBlocked = true; }
+ );
+
+ if(!avatarBlocked)
+ {
+ yield break;
+ }
+
+ VRC.Core.Logger.LogFormat(
+ "Avatar hidden due to low performance rating: [{0}] {1} - minimum setting: {2}",
+ perfStats.avatarName,
+ perfStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.Overall),
+ minPerfRating
+ );
+
+ onBlock();
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static PerformanceScannerSet GetPerformanceScannerSet()
+ {
+ PerformanceScannerSet performanceScannerSet;
+ if(VRC.ValidationHelpers.IsStandalonePlatform())
+ {
+ performanceScannerSet = Resources.Load<PerformanceScannerSet>("Validation/Performance/ScannerSets/PerformanceScannerSet_Windows");
+ }
+ else
+ {
+ performanceScannerSet = Resources.Load<PerformanceScannerSet>("Validation/Performance/ScannerSets/PerformanceScannerSet_Quest");
+ }
+
+ return performanceScannerSet;
+ }
+
+ private static PerformanceFilterSet GetPerformanceFilterSet()
+ {
+ PerformanceFilterSet performanceFilterSet;
+ if(VRC.ValidationHelpers.IsStandalonePlatform())
+ {
+ performanceFilterSet = Resources.Load<PerformanceFilterSet>("Validation/Performance/FilterSets/PerformanceFilterSet_Windows");
+ }
+ else
+ {
+ performanceFilterSet = Resources.Load<PerformanceFilterSet>("Validation/Performance/FilterSets/PerformanceFilterSet_Quest");
+ }
+
+ return performanceFilterSet;
+ }
+
+ private static bool ShouldIgnoreComponentInternal(Component component)
+ {
+ if(Application.isEditor)
+ {
+ if(component == null)
+ {
+ return false;
+ }
+
+ if(component.CompareTag("EditorOnly"))
+ {
+ return true;
+ }
+ }
+
+ if(ShouldIgnoreComponent != null)
+ {
+ return ShouldIgnoreComponent(component);
+ }
+
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs.meta
new file mode 100644
index 00000000..8d06a338
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformance.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 15ecac6f7fdc1bc4fb723fee6f4635dd
+timeCreated: 1540944864
+licenseType: Pro
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs
new file mode 100644
index 00000000..847ac62c
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs
@@ -0,0 +1,37 @@
+namespace VRC.SDKBase.Validation.Performance
+{
+ public enum AvatarPerformanceCategory
+ {
+ None,
+
+ Overall,
+
+ DownloadSize,
+ PolyCount,
+ AABB,
+ SkinnedMeshCount,
+ MeshCount,
+ MaterialCount,
+ DynamicBoneComponentCount,
+ DynamicBoneSimulatedBoneCount,
+ DynamicBoneColliderCount,
+ DynamicBoneCollisionCheckCount,
+ AnimatorCount,
+ BoneCount,
+ LightCount,
+ ParticleSystemCount,
+ ParticleTotalCount,
+ ParticleMaxMeshPolyCount,
+ ParticleTrailsEnabled,
+ ParticleCollisionEnabled,
+ TrailRendererCount,
+ LineRendererCount,
+ ClothCount,
+ ClothMaxVertices,
+ PhysicsColliderCount,
+ PhysicsRigidbodyCount,
+ AudioSourceCount,
+
+ AvatarPerformanceCategoryCount
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs.meta
new file mode 100644
index 00000000..449a3394
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/AvatarPerformanceCategory.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f1ce994297384ff1bc330196df61b7ca
+timeCreated: 1561267912 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters.meta
new file mode 100644
index 00000000..b19c2f04
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bf5b2b0296bbecc4dba509652ba3eb24
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs
new file mode 100644
index 00000000..9896fcc9
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs
@@ -0,0 +1,109 @@
+using System.Collections;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Filters
+{
+ public abstract class AbstractPerformanceFilter : ScriptableObject
+ {
+ public abstract IEnumerator ApplyPerformanceFilter(
+ GameObject avatarObject,
+ AvatarPerformanceStats perfStats,
+ PerformanceRating ratingLimit,
+ AvatarPerformance.IgnoreDelegate shouldIgnoreComponent,
+ AvatarPerformance.FilterBlockCallback onBlock
+ );
+
+ protected static IEnumerator RemoveComponentsOfTypeEnumerator<T>(GameObject target) where T : Component
+ {
+ if(target == null)
+ {
+ yield break;
+ }
+
+ foreach(T targetComponent in target.GetComponentsInChildren<T>(true))
+ {
+ if(targetComponent == null || targetComponent.gameObject == null)
+ {
+ continue;
+ }
+
+ #if VERBOSE_COMPONENT_REMOVAL
+ Debug.LogWarningFormat("Removing {0} comp from {1}", targetComponent.GetType().Name, targetComponent.gameObject.name);
+ #endif
+
+ yield return RemoveComponent(targetComponent);
+ }
+ }
+
+ protected static IEnumerator RemoveComponent(Component targetComponent)
+ {
+ yield return RemoveDependencies(targetComponent);
+
+ Destroy(targetComponent);
+ yield return null;
+ }
+
+ protected static IEnumerator RemoveDependencies(Component targetComponent)
+ {
+ if(targetComponent == null)
+ {
+ yield break;
+ }
+
+ Component[] siblingComponents = targetComponent.GetComponents<Component>();
+ if(siblingComponents == null || siblingComponents.Length == 0)
+ {
+ yield break;
+ }
+
+ System.Type componentType = targetComponent.GetType();
+ foreach(Component siblingComponent in siblingComponents)
+ {
+ if(siblingComponent == null)
+ {
+ continue;
+ }
+
+ bool deleteMe = false;
+ object[] requireComponentAttributes = siblingComponent.GetType().GetCustomAttributes(typeof(RequireComponent), true);
+ if(requireComponentAttributes.Length == 0)
+ {
+ continue;
+ }
+
+ foreach(var requireComponentObject in requireComponentAttributes)
+ {
+ RequireComponent requireComponentAttribute = requireComponentObject as RequireComponent;
+ if(requireComponentAttribute == null)
+ {
+ continue;
+ }
+
+ if(
+ requireComponentAttribute.m_Type0 != componentType &&
+ requireComponentAttribute.m_Type1 != componentType &&
+ requireComponentAttribute.m_Type2 != componentType
+ )
+ {
+ continue;
+ }
+
+ deleteMe = true;
+ break;
+ }
+
+ if(!deleteMe)
+ {
+ continue;
+ }
+
+ #if VERBOSE_COMPONENT_REMOVAL
+ Debug.LogWarningFormat("Deleting component dependency {0} found on {1}", siblingComponent.GetType().Name, targetComponent.gameObject.name);
+ #endif
+
+ yield return RemoveComponent(siblingComponent);
+ }
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs.meta
new file mode 100644
index 00000000..6987057b
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Filters/AbstractPerformanceFilter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: abda65e062e44213aa3e1f4c82b400a8
+timeCreated: 1563937347 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs
new file mode 100644
index 00000000..e5acd99f
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs
@@ -0,0 +1,33 @@
+using UnityEngine;
+
+namespace VRC.SDKBase.Validation.Performance
+{
+ public static class MeshUtils
+ {
+ private const uint INDICES_PER_TRIANGLE = 3U;
+
+ public static uint GetMeshTriangleCount(Mesh sourceMesh)
+ {
+ if(sourceMesh == null)
+ {
+ return 0;
+ }
+
+ // We can't use GetIndexCount if the mesh isn't readable so just return a huge number.
+ // The SDK Control Panel should show a warning in this case.
+ if(!sourceMesh.isReadable)
+ {
+ return uint.MaxValue;
+ }
+
+ uint count = 0;
+ for(int i = 0; i < sourceMesh.subMeshCount; i++)
+ {
+ uint indexCount = sourceMesh.GetIndexCount(i);
+ count += indexCount / INDICES_PER_TRIANGLE;
+ }
+
+ return count;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs.meta
new file mode 100644
index 00000000..4658d3db
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/MeshUtils.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f28c978154794266b38d686139c6b872
+timeCreated: 1561249515 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs
new file mode 100644
index 00000000..b2637485
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs
@@ -0,0 +1,46 @@
+using System.Collections;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Filters;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New PerformanceFilterSet",
+ menuName = "VRC Scriptable Objects/Performance/PerformanceFilterSet"
+ )]
+ #endif
+ public class PerformanceFilterSet : ScriptableObject
+ {
+ public AbstractPerformanceFilter[] performanceFilters;
+
+ public IEnumerator ApplyPerformanceFilters(
+ GameObject avatarObject,
+ AvatarPerformanceStats perfStats,
+ PerformanceRating ratingLimit,
+ AvatarPerformance.IgnoreDelegate shouldIgnoreComponent,
+ AvatarPerformance.FilterBlockCallback onBlock
+ )
+ {
+ foreach(AbstractPerformanceFilter performanceFilter in performanceFilters)
+ {
+ if(performanceFilter == null)
+ {
+ continue;
+ }
+
+ bool avatarBlocked = false;
+ yield return performanceFilter.ApplyPerformanceFilter(avatarObject, perfStats, ratingLimit, shouldIgnoreComponent, () => { avatarBlocked = true; });
+
+ if(!avatarBlocked)
+ {
+ continue;
+ }
+
+ onBlock();
+ break;
+ }
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs.meta
new file mode 100644
index 00000000..dce1427b
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceFilterSet.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8cdca9d06d1b4732b9ccb82a38bb8d9c
+timeCreated: 1563939583 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs
new file mode 100644
index 00000000..d132fe4a
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs
@@ -0,0 +1,12 @@
+namespace VRC.SDKBase.Validation.Performance
+{
+ public enum PerformanceInfoDisplayLevel
+ {
+ None,
+
+ Verbose,
+ Info,
+ Warning,
+ Error
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs.meta
new file mode 100644
index 00000000..c1ea78c5
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceInfoDisplayLevel.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a5ed7498cb1a46c78eab031f5f32448c
+timeCreated: 1561267918 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs
new file mode 100644
index 00000000..901d8494
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs
@@ -0,0 +1,12 @@
+namespace VRC.SDKBase.Validation.Performance
+{
+ public enum PerformanceRating
+ {
+ None = 0,
+ Excellent = 1,
+ Good = 2,
+ Medium = 3,
+ Poor = 4,
+ VeryPoor = 5
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs.meta
new file mode 100644
index 00000000..0175bfdb
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceRating.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5019a55ee9e2404c81bc61a39a010d8d
+timeCreated: 1561267904 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs
new file mode 100644
index 00000000..716c66f7
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs
@@ -0,0 +1,44 @@
+using System.Collections;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Scanners;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New PerformanceScannerSet",
+ menuName = "VRC Scriptable Objects/Performance/PerformanceScannerSet"
+ )]
+ #endif
+ public class PerformanceScannerSet : ScriptableObject
+ {
+ public AbstractPerformanceScanner[] performanceScanners;
+
+ public void RunPerformanceScan(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ foreach(AbstractPerformanceScanner performanceScanner in performanceScanners)
+ {
+ if(performanceScanner == null)
+ {
+ continue;
+ }
+
+ performanceScanner.RunPerformanceScan(avatarObject, perfStats, shouldIgnoreComponent);
+ }
+ }
+
+ public IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ foreach(AbstractPerformanceScanner performanceScanner in performanceScanners)
+ {
+ if(performanceScanner == null)
+ {
+ continue;
+ }
+
+ yield return performanceScanner.RunPerformanceScanEnumerator(avatarObject, perfStats, shouldIgnoreComponent);
+ }
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs.meta
new file mode 100644
index 00000000..7a3b582a
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/PerformanceScannerSet.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4afb61f36d144fc381114cd7f78d13e7
+timeCreated: 1561259704 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners.meta
new file mode 100644
index 00000000..3029e53c
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: edddcc1b6de3f554e8a13a0b94d47b96
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs
new file mode 100644
index 00000000..9face97c
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Profiling;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ public abstract class AbstractPerformanceScanner : ScriptableObject
+ {
+ private const int MAXIMUM_COMPONENT_SCANS_PER_FRAME = 10;
+ private static int _componentScansThisFrame = 0;
+ private static int _componentScansFrameNumber = 0;
+
+ private readonly Stack<IEnumerator> _coroutines = new Stack<IEnumerator>();
+
+ private bool _limitComponentScansPerFrame = true;
+
+ public abstract IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent);
+
+ public void RunPerformanceScan(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ _limitComponentScansPerFrame = false;
+
+ try
+ {
+ _coroutines.Push(RunPerformanceScanEnumerator(avatarObject, perfStats, shouldIgnoreComponent));
+ while(_coroutines.Count > 0)
+ {
+ IEnumerator currentCoroutine = _coroutines.Peek();
+ if(currentCoroutine.MoveNext())
+ {
+ IEnumerator nestedCoroutine = currentCoroutine.Current as IEnumerator;
+ if(nestedCoroutine != null)
+ {
+ _coroutines.Push(nestedCoroutine);
+ }
+ }
+ else
+ {
+ _coroutines.Pop();
+ }
+ }
+
+ _coroutines.Clear();
+ }
+ finally
+ {
+ _limitComponentScansPerFrame = true;
+ }
+ }
+
+ protected IEnumerator ScanAvatarForComponentsOfType(Type componentType, GameObject avatarObject, List<Component> destinationBuffer)
+ {
+ yield return HandleComponentScansPerFrameLimit();
+
+ Profiler.BeginSample("Component Scan");
+ destinationBuffer.Clear();
+ destinationBuffer.AddRange(avatarObject.GetComponentsInChildren(componentType, true));
+ Profiler.EndSample();
+ }
+
+ protected IEnumerator ScanAvatarForComponentsOfType<T>(GameObject avatarObject, List<T> destinationBuffer)
+ {
+ yield return HandleComponentScansPerFrameLimit();
+
+ Profiler.BeginSample("Component Scan");
+ destinationBuffer.Clear();
+ avatarObject.GetComponentsInChildren(true, destinationBuffer);
+ Profiler.EndSample();
+ yield return null;
+ }
+
+ private IEnumerator HandleComponentScansPerFrameLimit()
+ {
+ if(!_limitComponentScansPerFrame)
+ {
+ yield break;
+ }
+
+ while(_componentScansThisFrame >= MAXIMUM_COMPONENT_SCANS_PER_FRAME)
+ {
+ if(Time.frameCount > _componentScansFrameNumber)
+ {
+ _componentScansFrameNumber = Time.frameCount;
+ _componentScansThisFrame = 0;
+ break;
+ }
+
+ yield return null;
+ }
+
+ _componentScansThisFrame++;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs.meta
new file mode 100644
index 00000000..357ef0f0
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AbstractPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0bd0691a021844f49444a04a959d6328
+timeCreated: 1561279121 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs
new file mode 100644
index 00000000..6aa73010
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs
@@ -0,0 +1,44 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New AnimatorPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/AnimatorPerformanceScanner"
+ )]
+ #endif
+ public sealed class AnimatorPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ int animatorCount = 0;
+
+ // Animators
+ List<Animator> animatorBuffer = new List<Animator>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, animatorBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ animatorBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ // ReSharper disable once UselessBinaryOperation
+ animatorCount += animatorBuffer.Count;
+
+ // Animations
+ List<UnityEngine.Animation> animationBuffer = new List<UnityEngine.Animation>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, animationBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ animationBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ animatorCount += animationBuffer.Count;
+
+ perfStats.animatorCount = animatorCount;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs.meta
new file mode 100644
index 00000000..edec0a0f
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AnimatorPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 08c8e931d0544866a0f626855d9c1841
+timeCreated: 1561251363 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs
new file mode 100644
index 00000000..a8a5944d
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs
@@ -0,0 +1,29 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New AudioPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/AudioPerformanceScanner"
+ )]
+ #endif
+ public sealed class AudioPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Audio Sources
+ List<AudioSource> audioSourceBuffer = new List<AudioSource>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, audioSourceBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ audioSourceBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ perfStats.audioSourceCount = audioSourceBuffer.Count;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs.meta
new file mode 100644
index 00000000..6cbd2a09
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/AudioPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b3a8bba736414d1aaa9e766da27b56b5
+timeCreated: 1561255618 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs
new file mode 100644
index 00000000..d8332184
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs
@@ -0,0 +1,48 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Profiling;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New ClothPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/ClothPerformanceScanner"
+ )]
+ #endif
+ public sealed class ClothPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Cloth
+ List<Cloth> clothBuffer = new List<Cloth>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, clothBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ clothBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ int totalClothVertices = 0;
+ foreach(Cloth cloth in clothBuffer)
+ {
+ if(cloth == null)
+ {
+ continue;
+ }
+
+ Vector3[] clothVertices = cloth.vertices;
+ if(clothVertices == null)
+ {
+ continue;
+ }
+
+ totalClothVertices += clothVertices.Length;
+ }
+
+ perfStats.clothCount = clothBuffer.Count;
+ perfStats.clothMaxVertices = totalClothVertices;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs.meta
new file mode 100644
index 00000000..b04d8aef
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ClothPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0cec88b5a46f459195f10a2f11fddb2f
+timeCreated: 1561255843 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs
new file mode 100644
index 00000000..860df33a
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using UnityEngine;
+using UnityEngine.Profiling;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New DynamicBonePerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/DynamicBonePerformanceScanner"
+ )]
+ #endif
+ public sealed class DynamicBonePerformanceScanner : AbstractPerformanceScanner
+ {
+ private Type _dynamicBoneType;
+ private FieldInfo _dynamicBoneRootFieldInfo;
+ private FieldInfo _dynamicBoneExclusionsFieldInfo;
+ private FieldInfo _dynamicBoneCollidersFieldInfo;
+ private FieldInfo _dynamicBoneEndLengthFieldInfo;
+ private FieldInfo _dynamicBoneEndOffsetFieldInfo;
+
+ private void Awake()
+ {
+ FindDynamicBoneTypes();
+ }
+
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ if(_dynamicBoneType == null)
+ {
+ yield break;
+ }
+
+ // Dynamic Bone as Component
+ List<Component> dynamicBoneComponentBuffer = new List<Component>();
+ List<object> dynamicBoneColliderObjectBuffer = new List<object>();
+ yield return ScanAvatarForComponentsOfType(_dynamicBoneType, avatarObject, dynamicBoneComponentBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ dynamicBoneComponentBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ int totalSimulatedBoneCount = 0;
+ int totalCollisionChecks = 0;
+
+ Profiler.BeginSample("Analyze Dynamic Bones");
+ foreach(Component dynamicBone in dynamicBoneComponentBuffer)
+ {
+ Profiler.BeginSample("Single Dynamic Bone Component");
+ int simulatedBones = 0;
+
+ // Add extra bones to the end of each chain if end bones are being used.
+ float endLength = (float)_dynamicBoneEndLengthFieldInfo.GetValue(dynamicBone);
+ Vector3 endOffset = (Vector3)_dynamicBoneEndOffsetFieldInfo.GetValue(dynamicBone);
+ bool hasEndBones = endLength > 0 || endOffset != Vector3.zero;
+
+ Transform root = (Transform)_dynamicBoneRootFieldInfo.GetValue(dynamicBone);
+ if(root != null)
+ {
+ List<Transform> exclusions = (List<Transform>)_dynamicBoneExclusionsFieldInfo.GetValue(dynamicBone);
+
+ // Calculate number of simulated bones for the hierarchy
+ simulatedBones = CountTransformsRecursively(root, exclusions, hasEndBones);
+ totalSimulatedBoneCount += simulatedBones;
+ }
+
+ int colliderListEntryCount = 0;
+ IList colliderList = (IList)_dynamicBoneCollidersFieldInfo.GetValue(dynamicBone);
+ if(colliderList != null)
+ {
+ foreach(object collider in colliderList)
+ {
+ colliderListEntryCount += 1;
+ if(collider != null && !dynamicBoneColliderObjectBuffer.Contains(collider))
+ {
+ dynamicBoneColliderObjectBuffer.Add(collider);
+ }
+ }
+ }
+
+ // The root bone is skipped in collision checks.
+ totalCollisionChecks += (simulatedBones - 1) * colliderListEntryCount;
+ Profiler.EndSample();
+ }
+
+ Profiler.EndSample();
+
+ yield return null;
+
+ perfStats.dynamicBoneComponentCount = dynamicBoneComponentBuffer.Count;
+ perfStats.dynamicBoneSimulatedBoneCount = totalSimulatedBoneCount;
+ perfStats.dynamicBoneColliderCount = dynamicBoneColliderObjectBuffer.Count;
+ perfStats.dynamicBoneCollisionCheckCount = totalCollisionChecks;
+ }
+
+ private void FindDynamicBoneTypes()
+ {
+ if(_dynamicBoneType != null)
+ {
+ return;
+ }
+
+ Type dyBoneType = ValidationUtils.GetTypeFromName("DynamicBone");
+ if(dyBoneType == null)
+ {
+ return;
+ }
+
+ Type dyBoneColliderType = ValidationUtils.GetTypeFromName("DynamicBoneColliderBase") ?? ValidationUtils.GetTypeFromName("DynamicBoneCollider");
+ if(dyBoneColliderType == null)
+ {
+ return;
+ }
+
+ FieldInfo rootFieldInfo = dyBoneType.GetField("m_Root", BindingFlags.Public | BindingFlags.Instance);
+ if(rootFieldInfo == null || rootFieldInfo.FieldType != typeof(Transform))
+ {
+ return;
+ }
+
+ FieldInfo exclusionsFieldInfo = dyBoneType.GetField("m_Exclusions", BindingFlags.Public | BindingFlags.Instance);
+ if(exclusionsFieldInfo == null || exclusionsFieldInfo.FieldType != typeof(List<Transform>))
+ {
+ return;
+ }
+
+ FieldInfo collidersFieldInfo = dyBoneType.GetField("m_Colliders", BindingFlags.Public | BindingFlags.Instance);
+ if(collidersFieldInfo == null || collidersFieldInfo.FieldType.GetGenericTypeDefinition() != typeof(List<>) ||
+ collidersFieldInfo.FieldType.GetGenericArguments().Single() != dyBoneColliderType)
+ {
+ return;
+ }
+
+ FieldInfo endLengthFieldInfo = dyBoneType.GetField("m_EndLength", BindingFlags.Public | BindingFlags.Instance);
+ if(endLengthFieldInfo == null || endLengthFieldInfo.FieldType != typeof(float))
+ {
+ return;
+ }
+
+ FieldInfo endOffsetFieldInfo = dyBoneType.GetField("m_EndOffset", BindingFlags.Public | BindingFlags.Instance);
+ if(endOffsetFieldInfo == null || endOffsetFieldInfo.FieldType != typeof(Vector3))
+ {
+ return;
+ }
+
+ _dynamicBoneType = dyBoneType;
+ _dynamicBoneRootFieldInfo = rootFieldInfo;
+ _dynamicBoneExclusionsFieldInfo = exclusionsFieldInfo;
+ _dynamicBoneCollidersFieldInfo = collidersFieldInfo;
+ _dynamicBoneEndLengthFieldInfo = endLengthFieldInfo;
+ _dynamicBoneEndOffsetFieldInfo = endOffsetFieldInfo;
+ }
+
+ // Like DynamicBone itself exclusions only apply to children of the current bone.
+ // This means the root bone itself never excluded.
+ private static int CountTransformsRecursively(Transform transform, List<Transform> exclusions, bool addEndBones)
+ {
+ if(transform == null)
+ {
+ return 0;
+ }
+
+ int count = 1;
+ int childCount = transform.childCount;
+ if(childCount > 0)
+ {
+ foreach(Transform child in transform)
+ {
+ if(exclusions == null || !exclusions.Contains(child))
+ {
+ count += CountTransformsRecursively(child, exclusions, addEndBones);
+ }
+ }
+ }
+ else
+ {
+ if(addEndBones)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs.meta
new file mode 100644
index 00000000..60f0db5a
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/DynamicBonePerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a226df494ef04404a9a47c714822ab9f
+timeCreated: 1561251560 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs
new file mode 100644
index 00000000..8d86c6b5
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs
@@ -0,0 +1,29 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New LightPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/LightPerformanceScanner"
+ )]
+ #endif
+ public sealed class LightPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Lights
+ List<Light> lightBuffer = new List<Light>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, lightBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ lightBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ perfStats.lightCount = lightBuffer.Count;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs.meta
new file mode 100644
index 00000000..9c346056
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LightPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 405778fdc32c44c1bb9fdd0476fb0007
+timeCreated: 1561256390 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs
new file mode 100644
index 00000000..1d1faa85
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs
@@ -0,0 +1,31 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New LineRendererPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/LineRendererPerformanceScanner"
+ )]
+ #endif
+ public sealed class LineRendererPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Line Renderers
+ List<LineRenderer> lineRendererBuffer = new List<LineRenderer>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, lineRendererBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ lineRendererBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ int numLineRenderers = lineRendererBuffer.Count;
+ perfStats.lineRendererCount = numLineRenderers;
+ perfStats.materialCount = perfStats.materialCount.GetValueOrDefault() + numLineRenderers;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs.meta
new file mode 100644
index 00000000..79875a56
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/LineRendererPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ec87392b85844f7bb526a48ec866a8f0
+timeCreated: 1561253047 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs
new file mode 100644
index 00000000..9b5b1201
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Profiling;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New MeshPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/MeshPerformanceScanner"
+ )]
+ #endif
+ public sealed class MeshPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Renderers
+ List<Renderer> rendererBuffer = new List<Renderer>(16);
+ yield return ScanAvatarForComponentsOfType(avatarObject, rendererBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ rendererBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ yield return AnalyzeGeometry(avatarObject, rendererBuffer, perfStats);
+ AnalyzeMeshRenderers(rendererBuffer, perfStats);
+ AnalyzeSkinnedMeshRenderers(rendererBuffer, perfStats);
+
+
+ yield return null;
+ }
+
+ private static uint CalculateRendererPolyCount(Renderer renderer)
+ {
+ Mesh sharedMesh = null;
+ SkinnedMeshRenderer skinnedMeshRenderer = renderer as SkinnedMeshRenderer;
+ if(skinnedMeshRenderer != null)
+ {
+ sharedMesh = skinnedMeshRenderer.sharedMesh;
+ }
+
+ if(sharedMesh == null)
+ {
+ MeshRenderer meshRenderer = renderer as MeshRenderer;
+ if(meshRenderer != null)
+ {
+ MeshFilter meshFilter = meshRenderer.GetComponent<MeshFilter>();
+ if(meshFilter != null)
+ {
+ sharedMesh = meshFilter.sharedMesh;
+ }
+ }
+ }
+
+ if(sharedMesh == null)
+ {
+ return 0;
+ }
+
+ return MeshUtils.GetMeshTriangleCount(sharedMesh);
+ }
+
+ private static bool RendererHasMesh(Renderer renderer)
+ {
+ MeshRenderer meshRenderer = renderer as MeshRenderer;
+ if(meshRenderer != null)
+ {
+ MeshFilter meshFilter = meshRenderer.GetComponent<MeshFilter>();
+ if(meshFilter == null)
+ {
+ return false;
+ }
+
+ return meshFilter.sharedMesh != null;
+ }
+
+ SkinnedMeshRenderer skinnedMeshRenderer = renderer as SkinnedMeshRenderer;
+ if(skinnedMeshRenderer != null)
+ {
+ return skinnedMeshRenderer.sharedMesh != null;
+ }
+
+ return false;
+ }
+
+ private IEnumerator AnalyzeGeometry(GameObject avatarObject, IEnumerable<Renderer> renderers, AvatarPerformanceStats perfStats)
+ {
+ List<Renderer> lodGroupRendererIgnoreBuffer = new List<Renderer>(16);
+ List<LODGroup> lodBuffer = new List<LODGroup>(16);
+
+ ulong polyCount = 0;
+ Bounds bounds = new Bounds(avatarObject.transform.position, Vector3.zero);
+
+ yield return ScanAvatarForComponentsOfType(avatarObject, lodBuffer);
+ try
+ {
+ foreach(LODGroup lodGroup in lodBuffer)
+ {
+ LOD[] lodLevels = lodGroup.GetLODs();
+
+ ulong highestLodPolyCount = 0;
+ foreach(LOD lod in lodLevels)
+ {
+ uint thisLodPolyCount = 0;
+ foreach(Renderer renderer in lod.renderers)
+ {
+ lodGroupRendererIgnoreBuffer.Add(renderer);
+ checked
+ {
+ thisLodPolyCount += CalculateRendererPolyCount(renderer);
+ }
+ }
+
+ if(thisLodPolyCount > highestLodPolyCount)
+ {
+ highestLodPolyCount = thisLodPolyCount;
+ }
+ }
+
+ checked
+ {
+ polyCount += highestLodPolyCount;
+ }
+ }
+ }
+ catch(OverflowException e)
+ {
+ VRC.Core.Logger.Log("Overflow exception while analyzing geometry, assuming max value:" + e.ToString(), VRC.Core.DebugLevel.All);
+ polyCount = uint.MaxValue;
+ }
+
+ Profiler.BeginSample("Calculate Total Polygon Count and Bounds");
+ foreach(Renderer renderer in renderers)
+ {
+ Profiler.BeginSample("Single Renderer");
+ if(renderer is MeshRenderer || renderer is SkinnedMeshRenderer)
+ {
+ if(!RendererHasMesh(renderer))
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ bounds.Encapsulate(renderer.bounds);
+ }
+
+ if(lodGroupRendererIgnoreBuffer.Contains(renderer))
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ polyCount += CalculateRendererPolyCount(renderer);
+ Profiler.EndSample();
+ }
+
+ Profiler.EndSample();
+
+ bounds.center -= avatarObject.transform.position;
+
+ lodGroupRendererIgnoreBuffer.Clear();
+ lodBuffer.Clear();
+
+ perfStats.polyCount = polyCount > int.MaxValue ? int.MaxValue : (int)polyCount;
+ perfStats.aabb = bounds;
+ }
+
+ private static void AnalyzeSkinnedMeshRenderers(IEnumerable<Renderer> renderers, AvatarPerformanceStats perfStats)
+ {
+ Profiler.BeginSample("AnalyzeSkinnedMeshRenderers");
+ int count = 0;
+ int materialSlots = 0;
+ int skinnedBoneCount = 0;
+ HashSet<Transform> transformIgnoreBuffer = new HashSet<Transform>();
+
+ foreach(Renderer renderer in renderers)
+ {
+ Profiler.BeginSample("Analyze SkinnedMeshRenderer");
+ SkinnedMeshRenderer skinnedMeshRenderer = renderer as SkinnedMeshRenderer;
+ if(skinnedMeshRenderer == null)
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ count++;
+
+ Mesh sharedMesh = skinnedMeshRenderer.sharedMesh;
+ if(sharedMesh != null)
+ {
+ materialSlots += sharedMesh.subMeshCount;
+ }
+
+ // bone count
+ Profiler.BeginSample("Count Bones");
+ Transform[] bones = skinnedMeshRenderer.bones;
+ foreach(Transform bone in bones)
+ {
+ Profiler.BeginSample("Count Bone");
+ if(bone == null || transformIgnoreBuffer.Contains(bone))
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ transformIgnoreBuffer.Add(bone);
+ skinnedBoneCount++;
+ Profiler.EndSample();
+ }
+
+ Profiler.EndSample();
+
+ Profiler.EndSample();
+ }
+
+ transformIgnoreBuffer.Clear();
+ Profiler.EndSample();
+
+ perfStats.skinnedMeshCount = count;
+ perfStats.boneCount = skinnedBoneCount;
+ perfStats.materialCount = perfStats.materialCount.GetValueOrDefault() + materialSlots;
+ }
+
+ private static void AnalyzeMeshRenderers(IEnumerable<Renderer> renderers, AvatarPerformanceStats perfStats)
+ {
+ Profiler.BeginSample("AnalyzeMeshRenderers");
+ int count = 0;
+ int materialSlots = 0;
+ foreach(Renderer renderer in renderers)
+ {
+ Profiler.BeginSample("Analyze MeshRenderer");
+ MeshRenderer meshRenderer = renderer as MeshRenderer;
+ if(meshRenderer == null)
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ count++;
+
+ Profiler.BeginSample("Get MeshFilter");
+ MeshFilter meshFilter = meshRenderer.GetComponent<MeshFilter>();
+ Profiler.EndSample();
+ if(meshFilter == null)
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ Mesh sharedMesh = meshFilter.sharedMesh;
+ if(sharedMesh != null)
+ {
+ materialSlots += sharedMesh.subMeshCount;
+ }
+ }
+
+ Profiler.EndSample();
+
+ perfStats.meshCount = count;
+ perfStats.materialCount = perfStats.materialCount.GetValueOrDefault() + materialSlots;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs.meta
new file mode 100644
index 00000000..ba7c1507
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/MeshPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 38bca10261df4ddfa10cff3b3bbb9428
+timeCreated: 1561249198 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs
new file mode 100644
index 00000000..3d7acea8
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs
@@ -0,0 +1,114 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Profiling;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New ParticlePerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/ParticlePerformanceScanner"
+ )]
+ #endif
+ public sealed class ParticlePerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Particle Systems
+ List<ParticleSystem> particleSystemBuffer = new List<ParticleSystem>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, particleSystemBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ particleSystemBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ AnalyzeParticleSystemRenderers(particleSystemBuffer, perfStats);
+
+ yield return null;
+ }
+
+ private static void AnalyzeParticleSystemRenderers(IEnumerable<ParticleSystem> particleSystems, AvatarPerformanceStats perfStats)
+ {
+ int particleSystemCount = 0;
+ ulong particleTotalCount = 0;
+ ulong particleTotalMaxMeshPolyCount = 0;
+ bool particleTrailsEnabled = false;
+ bool particleCollisionEnabled = false;
+ int materialSlots = 0;
+
+ Profiler.BeginSample("AnalyzeParticleSystemRenderers");
+ foreach(ParticleSystem particleSystem in particleSystems)
+ {
+ Profiler.BeginSample("Single Particle System");
+ int particleCount = particleSystem.main.maxParticles;
+ if(particleCount <= 0)
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ particleSystemCount++;
+ particleTotalCount += (uint)particleCount;
+
+ ParticleSystemRenderer particleSystemRenderer = particleSystem.GetComponent<ParticleSystemRenderer>();
+ if(particleSystemRenderer == null)
+ {
+ Profiler.EndSample();
+ continue;
+ }
+
+ materialSlots++;
+
+ // mesh particles
+ if(particleSystemRenderer.renderMode == ParticleSystemRenderMode.Mesh && particleSystemRenderer.meshCount > 0)
+ {
+ uint highestPolyCount = 0;
+
+ Mesh[] meshes = new Mesh[particleSystemRenderer.meshCount];
+ int particleRendererMeshCount = particleSystemRenderer.GetMeshes(meshes);
+ for(int meshIndex = 0; meshIndex < particleRendererMeshCount; meshIndex++)
+ {
+ Mesh mesh = meshes[meshIndex];
+ if(mesh == null)
+ {
+ continue;
+ }
+
+ uint polyCount = MeshUtils.GetMeshTriangleCount(mesh);
+ if(polyCount > highestPolyCount)
+ {
+ highestPolyCount = polyCount;
+ }
+ }
+
+ ulong maxMeshParticlePolyCount = (uint)particleCount * highestPolyCount;
+ particleTotalMaxMeshPolyCount += maxMeshParticlePolyCount;
+ }
+
+ if(particleSystem.trails.enabled)
+ {
+ particleTrailsEnabled = true;
+ materialSlots++;
+ }
+
+ if(particleSystem.collision.enabled)
+ {
+ particleCollisionEnabled = true;
+ }
+
+ Profiler.EndSample();
+ }
+
+ Profiler.EndSample();
+
+ perfStats.particleSystemCount = particleSystemCount;
+ perfStats.particleTotalCount = particleTotalCount > int.MaxValue ? int.MaxValue : (int)particleTotalCount;
+ perfStats.particleMaxMeshPolyCount = particleTotalMaxMeshPolyCount > int.MaxValue ? int.MaxValue : (int)particleTotalMaxMeshPolyCount;
+ perfStats.particleTrailsEnabled = particleTrailsEnabled;
+ perfStats.particleCollisionEnabled = particleCollisionEnabled;
+ perfStats.materialCount = perfStats.materialCount.GetValueOrDefault() + materialSlots;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs.meta
new file mode 100644
index 00000000..54556038
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/ParticlePerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 10723e354ff14f98a49ab797b3f005e6
+timeCreated: 1561250267 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs
new file mode 100644
index 00000000..c6b643da
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs
@@ -0,0 +1,64 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Profiling;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New PhysicsPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/PhysicsPerformanceScanner"
+ )]
+ #endif
+ public sealed class PhysicsPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Colliders
+ List<Collider> colliderBuffer = new List<Collider>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, colliderBuffer);
+ colliderBuffer.RemoveAll(
+ o =>
+ {
+ if(shouldIgnoreComponent != null && shouldIgnoreComponent(o))
+ {
+ return true;
+ }
+
+ if(o.GetComponent<VRC.SDKBase.VRCStation>() != null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ );
+
+ perfStats.physicsColliderCount = colliderBuffer.Count;
+
+ // Rigidbodies
+ List<Rigidbody> rigidbodyBuffer = new List<Rigidbody>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, rigidbodyBuffer);
+ rigidbodyBuffer.RemoveAll(
+ o =>
+ {
+ if(shouldIgnoreComponent != null && shouldIgnoreComponent(o))
+ {
+ return true;
+ }
+
+ if(o.GetComponent<VRC.SDKBase.VRCStation>() != null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ );
+
+ perfStats.physicsRigidbodyCount = rigidbodyBuffer.Count;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs.meta
new file mode 100644
index 00000000..ed550cf1
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/PhysicsPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 6a94ecdeecd04f85824cc3244be5712a
+timeCreated: 1561256481 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs
new file mode 100644
index 00000000..c75e4cae
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs
@@ -0,0 +1,31 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.SDKBase.Validation.Performance.Stats;
+
+namespace VRC.SDKBase.Validation.Performance.Scanners
+{
+ #if VRC_CLIENT
+ [CreateAssetMenu(
+ fileName = "New TrailRendererPerformanceScanner",
+ menuName = "VRC Scriptable Objects/Performance/Avatar/Scanners/TrailRendererPerformanceScanner"
+ )]
+ #endif
+ public sealed class TrailRendererPerformanceScanner : AbstractPerformanceScanner
+ {
+ public override IEnumerator RunPerformanceScanEnumerator(GameObject avatarObject, AvatarPerformanceStats perfStats, AvatarPerformance.IgnoreDelegate shouldIgnoreComponent)
+ {
+ // Trail Renderers
+ List<TrailRenderer> trailRendererBuffer = new List<TrailRenderer>();
+ yield return ScanAvatarForComponentsOfType(avatarObject, trailRendererBuffer);
+ if(shouldIgnoreComponent != null)
+ {
+ trailRendererBuffer.RemoveAll(c => shouldIgnoreComponent(c));
+ }
+
+ int numTrailRenderers = trailRendererBuffer.Count;
+ perfStats.trailRendererCount = numTrailRenderers;
+ perfStats.materialCount = perfStats.materialCount.GetValueOrDefault() + numTrailRenderers;
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs.meta
new file mode 100644
index 00000000..028dd032
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Scanners/TrailRendererPerformanceScanner.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2efd714b564547b4be1ebd1f2700668b
+timeCreated: 1561251810 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats.meta
new file mode 100644
index 00000000..39e091dc
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6970bf241e7266748992480464b59687
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs
new file mode 100644
index 00000000..44fd4041
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs
@@ -0,0 +1,757 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using UnityEngine;
+
+namespace VRC.SDKBase.Validation.Performance.Stats
+{
+ public class AvatarPerformanceStats
+ {
+ private delegate int ComparePerformanceStatsDelegate(AvatarPerformanceStats stats, AvatarPerformanceStatsLevel statsLevel);
+
+ #region Public Fields
+
+ public string avatarName;
+
+ public int? polyCount;
+ public Bounds? aabb;
+ public int? skinnedMeshCount;
+ public int? meshCount;
+ public int? materialCount;
+ public int? animatorCount;
+ public int? boneCount;
+ public int? lightCount;
+ public int? particleSystemCount;
+ public int? particleTotalCount;
+ public int? particleMaxMeshPolyCount;
+ public bool? particleTrailsEnabled;
+ public bool? particleCollisionEnabled;
+ public int? trailRendererCount;
+ public int? lineRendererCount;
+ public int? dynamicBoneComponentCount;
+ public int? dynamicBoneSimulatedBoneCount;
+ public int? dynamicBoneColliderCount;
+ public int? dynamicBoneCollisionCheckCount; // number of collider simulated bones excluding the root multiplied by the number of colliders
+ public int? clothCount;
+ public int? clothMaxVertices;
+ public int? physicsColliderCount;
+ public int? physicsRigidbodyCount;
+ public int? audioSourceCount;
+ public float? downloadSize;
+
+ #endregion
+
+ #region Private Fields
+
+ private readonly PerformanceRating[] _performanceRatingCache;
+
+ private static readonly ImmutableArray<AvatarPerformanceCategory> _performanceCategories = Enum.GetValues(typeof(AvatarPerformanceCategory))
+ .Cast<AvatarPerformanceCategory>()
+ .ToImmutableArray();
+
+ private static readonly Dictionary<AvatarPerformanceCategory, string> _performanceCategoryDisplayNames = new Dictionary<AvatarPerformanceCategory, string>
+ {
+ {AvatarPerformanceCategory.PolyCount, "Polygons"},
+ {AvatarPerformanceCategory.AABB, "Bounds"},
+ {AvatarPerformanceCategory.SkinnedMeshCount, "Skinned Meshes"},
+ {AvatarPerformanceCategory.MeshCount, "Meshes"},
+ {AvatarPerformanceCategory.MaterialCount, "Material Slots"},
+ {AvatarPerformanceCategory.AnimatorCount, "Animators"},
+ {AvatarPerformanceCategory.BoneCount, "Bones"},
+ {AvatarPerformanceCategory.LightCount, "Lights"},
+ {AvatarPerformanceCategory.ParticleSystemCount, "Particle Systems"},
+ {AvatarPerformanceCategory.ParticleTotalCount, "Total Max Particles"},
+ {AvatarPerformanceCategory.ParticleMaxMeshPolyCount, "Mesh Particle Max Polygons"},
+ {AvatarPerformanceCategory.ParticleTrailsEnabled, "Particle Trails Enabled"},
+ {AvatarPerformanceCategory.ParticleCollisionEnabled, "Particle Collision Enabled"},
+ {AvatarPerformanceCategory.TrailRendererCount, "Trail Renderers"},
+ {AvatarPerformanceCategory.LineRendererCount, "Line Renderers"},
+ {AvatarPerformanceCategory.DynamicBoneComponentCount, "Dynamic Bone Components"},
+ {AvatarPerformanceCategory.DynamicBoneSimulatedBoneCount, "Dynamic Bone Transforms"},
+ {AvatarPerformanceCategory.DynamicBoneColliderCount, "Dynamic Bone Colliders"},
+ {AvatarPerformanceCategory.DynamicBoneCollisionCheckCount, "Dynamic Bone Collision Check Count"},
+ {AvatarPerformanceCategory.ClothCount, "Cloths"},
+ {AvatarPerformanceCategory.ClothMaxVertices, "Total Cloth Vertices"},
+ {AvatarPerformanceCategory.PhysicsColliderCount, "Physics Colliders"},
+ {AvatarPerformanceCategory.PhysicsRigidbodyCount, "Physics Rigidbodies"},
+ {AvatarPerformanceCategory.AudioSourceCount, "Audio Sources"},
+ {AvatarPerformanceCategory.DownloadSize, "Download Size"},
+ };
+
+ private static readonly Dictionary<PerformanceRating, string> _performanceRatingDisplayNames = new Dictionary<PerformanceRating, string>
+ {
+ {PerformanceRating.None, "None"},
+ {PerformanceRating.Excellent, "Excellent"},
+ {PerformanceRating.Good, "Good"},
+ {PerformanceRating.Medium, "Medium"},
+ {PerformanceRating.Poor, "Poor"},
+ {PerformanceRating.VeryPoor, "VeryPoor"}
+ };
+
+ #endregion
+
+ #region Initialization
+
+ private static AvatarPerformanceStatsLevelSet _performanceStatsLevelSet = null;
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
+ public static void Initialize()
+ {
+ if(_performanceStatsLevelSet != null)
+ {
+ return;
+ }
+
+ _performanceStatsLevelSet = Resources.Load<AvatarPerformanceStatsLevelSet>(GetPlatformPerformanceStatLevels());
+ }
+
+ private static string GetPlatformPerformanceStatLevels()
+ {
+ #if UNITY_ANDROID
+ return "Validation/Performance/StatsLevels/Quest/AvatarPerformanceStatLevels_Quest";
+ #else
+ return "Validation/Performance/StatsLevels/Windows/AvatarPerformanceStatLevels_Windows";
+ #endif
+ }
+
+ #endregion
+
+ #region Constructors
+
+ public AvatarPerformanceStats()
+ {
+ _performanceRatingCache = new PerformanceRating[(int)AvatarPerformanceCategory.AvatarPerformanceCategoryCount];
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public void Reset()
+ {
+ avatarName = null;
+ polyCount = null;
+ aabb = null;
+ skinnedMeshCount = null;
+ meshCount = null;
+ materialCount = null;
+ animatorCount = null;
+ boneCount = null;
+ lightCount = null;
+ particleSystemCount = null;
+ particleTotalCount = null;
+ particleMaxMeshPolyCount = null;
+ particleTrailsEnabled = null;
+ particleCollisionEnabled = null;
+ trailRendererCount = null;
+ lineRendererCount = null;
+ dynamicBoneComponentCount = null;
+ dynamicBoneSimulatedBoneCount = null;
+ dynamicBoneColliderCount = null;
+ dynamicBoneCollisionCheckCount = null;
+ clothCount = null;
+ clothMaxVertices = null;
+ physicsColliderCount = null;
+ physicsRigidbodyCount = null;
+ audioSourceCount = null;
+ downloadSize = null;
+
+ for(int i = 0; i < (int)AvatarPerformanceCategory.AvatarPerformanceCategoryCount; i++)
+ {
+ _performanceRatingCache[i] = PerformanceRating.None;
+ }
+ }
+
+ public Snapshot GetSnapshot()
+ {
+ return new Snapshot(this);
+ }
+
+ public PerformanceRating GetPerformanceRatingForCategory(AvatarPerformanceCategory perfCategory)
+ {
+ if(_performanceRatingCache[(int)perfCategory] == PerformanceRating.None)
+ {
+ _performanceRatingCache[(int)perfCategory] = CalculatePerformanceRatingForCategory(perfCategory);
+ }
+
+ return _performanceRatingCache[(int)perfCategory];
+ }
+
+ public void CalculateAllPerformanceRatings()
+ {
+ for(int i = 0; i < _performanceRatingCache.Length; i++)
+ {
+ _performanceRatingCache[i] = PerformanceRating.None;
+ }
+
+ foreach(AvatarPerformanceCategory perfCategory in _performanceCategories)
+ {
+ if(perfCategory == AvatarPerformanceCategory.None ||
+ perfCategory == AvatarPerformanceCategory.AvatarPerformanceCategoryCount)
+ {
+ continue;
+ }
+
+ if(_performanceRatingCache[(int)perfCategory] == PerformanceRating.None)
+ {
+ _performanceRatingCache[(int)perfCategory] = CalculatePerformanceRatingForCategory(perfCategory);
+ }
+ }
+ }
+
+ public static string GetPerformanceCategoryDisplayName(AvatarPerformanceCategory category)
+ {
+ return _performanceCategoryDisplayNames[category];
+ }
+
+ public static string GetPerformanceRatingDisplayName(PerformanceRating rating)
+ {
+ return _performanceRatingDisplayNames[rating];
+ }
+
+ public static AvatarPerformanceStatsLevel GetStatLevelForRating(PerformanceRating rating)
+ {
+ switch(rating)
+ {
+ case PerformanceRating.None:
+ return _performanceStatsLevelSet.excellent;
+
+ case PerformanceRating.Excellent:
+ return _performanceStatsLevelSet.excellent;
+
+ case PerformanceRating.Good:
+ return _performanceStatsLevelSet.good;
+
+ case PerformanceRating.Medium:
+ return _performanceStatsLevelSet.medium;
+
+ case PerformanceRating.Poor:
+ return _performanceStatsLevelSet.poor;
+
+ case PerformanceRating.VeryPoor:
+ return _performanceStatsLevelSet.poor;
+
+ default:
+ return _performanceStatsLevelSet.excellent;
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private PerformanceRating CalculatePerformanceRatingForCategory(AvatarPerformanceCategory perfCategory)
+ {
+ switch(perfCategory)
+ {
+ case AvatarPerformanceCategory.Overall:
+ {
+ PerformanceRating maxRating = PerformanceRating.None;
+
+ foreach(AvatarPerformanceCategory category in _performanceCategories)
+ {
+ if(category == AvatarPerformanceCategory.None ||
+ category == AvatarPerformanceCategory.Overall ||
+ category == AvatarPerformanceCategory.AvatarPerformanceCategoryCount)
+ {
+ continue;
+ }
+
+ PerformanceRating rating = GetPerformanceRatingForCategory(category);
+ if(rating > maxRating)
+ {
+ maxRating = rating;
+ }
+ }
+
+ return maxRating;
+ }
+ case AvatarPerformanceCategory.PolyCount:
+ {
+ if(!polyCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.polyCount.GetValueOrDefault() - y.polyCount);
+ }
+ case AvatarPerformanceCategory.AABB:
+ {
+ if(!aabb.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating(
+ (x, y) =>
+ ApproxLessOrEqual(y.aabb.extents.x, 0.0f) || // -1 extents means "no AABB limit"
+ (
+ ApproxLessOrEqual(x.aabb.GetValueOrDefault().extents.x, y.aabb.extents.x) &&
+ ApproxLessOrEqual(x.aabb.GetValueOrDefault().extents.y, y.aabb.extents.y) &&
+ ApproxLessOrEqual(x.aabb.GetValueOrDefault().extents.z, y.aabb.extents.z))
+ ? -1
+ : 1
+ );
+ }
+ case AvatarPerformanceCategory.SkinnedMeshCount:
+ {
+ if(!skinnedMeshCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.skinnedMeshCount.GetValueOrDefault() - y.skinnedMeshCount);
+ }
+ case AvatarPerformanceCategory.MeshCount:
+ {
+ if(!meshCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.meshCount.GetValueOrDefault() - y.meshCount);
+ }
+ case AvatarPerformanceCategory.MaterialCount:
+ {
+ if(!materialCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.materialCount.GetValueOrDefault() - y.materialCount);
+ }
+ case AvatarPerformanceCategory.AnimatorCount:
+ {
+ if(!animatorCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.animatorCount.GetValueOrDefault() - y.animatorCount);
+ }
+ case AvatarPerformanceCategory.BoneCount:
+ {
+ if(!boneCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.boneCount.GetValueOrDefault() - y.boneCount);
+ }
+ case AvatarPerformanceCategory.LightCount:
+ {
+ if(!lightCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.lightCount.GetValueOrDefault() - y.lightCount);
+ }
+ case AvatarPerformanceCategory.ParticleSystemCount:
+ {
+ if(!particleSystemCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.particleSystemCount.GetValueOrDefault() - y.particleSystemCount);
+ }
+ case AvatarPerformanceCategory.ParticleTotalCount:
+ {
+ if(!particleTotalCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.particleTotalCount.GetValueOrDefault() - y.particleTotalCount);
+ }
+ case AvatarPerformanceCategory.ParticleMaxMeshPolyCount:
+ {
+ if(!particleMaxMeshPolyCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.particleMaxMeshPolyCount.GetValueOrDefault() - y.particleMaxMeshPolyCount);
+ }
+ case AvatarPerformanceCategory.ParticleTrailsEnabled:
+ {
+ if(!particleTrailsEnabled.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating(
+ (x, y) =>
+ {
+ if(x.particleTrailsEnabled == y.particleTrailsEnabled)
+ {
+ return 0;
+ }
+
+ return x.particleTrailsEnabled.GetValueOrDefault() ? 1 : -1;
+ });
+ }
+ case AvatarPerformanceCategory.ParticleCollisionEnabled:
+ {
+ if(!particleCollisionEnabled.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating(
+ (x, y) =>
+ {
+ if(x.particleCollisionEnabled == y.particleCollisionEnabled)
+ {
+ return 0;
+ }
+
+ return x.particleCollisionEnabled.GetValueOrDefault() ? 1 : -1;
+ });
+ }
+ case AvatarPerformanceCategory.TrailRendererCount:
+ {
+ if(!trailRendererCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.trailRendererCount.GetValueOrDefault() - y.trailRendererCount);
+ }
+ case AvatarPerformanceCategory.LineRendererCount:
+ {
+ if(!lineRendererCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.lineRendererCount.GetValueOrDefault() - y.lineRendererCount);
+ }
+ case AvatarPerformanceCategory.DynamicBoneComponentCount:
+ {
+ if(!dynamicBoneComponentCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.dynamicBoneComponentCount.GetValueOrDefault() - y.dynamicBoneComponentCount);
+ }
+ case AvatarPerformanceCategory.DynamicBoneSimulatedBoneCount:
+ {
+ if(!dynamicBoneSimulatedBoneCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.dynamicBoneSimulatedBoneCount.GetValueOrDefault() - y.dynamicBoneSimulatedBoneCount);
+ }
+ case AvatarPerformanceCategory.DynamicBoneColliderCount:
+ {
+ if(!dynamicBoneColliderCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.dynamicBoneColliderCount.GetValueOrDefault() - y.dynamicBoneColliderCount);
+ }
+ case AvatarPerformanceCategory.DynamicBoneCollisionCheckCount:
+ {
+ if(!dynamicBoneCollisionCheckCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.dynamicBoneCollisionCheckCount.GetValueOrDefault() - y.dynamicBoneCollisionCheckCount);
+ }
+ case AvatarPerformanceCategory.ClothCount:
+ {
+ if(!clothCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.clothCount.GetValueOrDefault() - y.clothCount);
+ }
+ case AvatarPerformanceCategory.ClothMaxVertices:
+ {
+ if(!clothMaxVertices.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.clothMaxVertices.GetValueOrDefault() - y.clothMaxVertices);
+ }
+ case AvatarPerformanceCategory.PhysicsColliderCount:
+ {
+ if(!physicsColliderCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.physicsColliderCount.GetValueOrDefault() - y.physicsColliderCount);
+ }
+ case AvatarPerformanceCategory.PhysicsRigidbodyCount:
+ {
+ if(!physicsRigidbodyCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.physicsRigidbodyCount.GetValueOrDefault() - y.physicsRigidbodyCount);
+ }
+ case AvatarPerformanceCategory.AudioSourceCount:
+ {
+ if(!audioSourceCount.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return CalculatePerformanceRating((x, y) => x.audioSourceCount.GetValueOrDefault() - y.audioSourceCount);
+ }
+ case AvatarPerformanceCategory.DownloadSize:
+ {
+ if(!downloadSize.HasValue)
+ {
+ return PerformanceRating.None;
+ }
+
+ return PerformanceRating.Excellent;
+ }
+ default:
+ {
+ return PerformanceRating.None;
+ }
+ }
+ }
+
+ private PerformanceRating CalculatePerformanceRating(ComparePerformanceStatsDelegate compareFn)
+ {
+ if(compareFn(this, _performanceStatsLevelSet.excellent) <= 0)
+ {
+ return PerformanceRating.Excellent;
+ }
+
+ if(compareFn(this, _performanceStatsLevelSet.good) <= 0)
+ {
+ return PerformanceRating.Good;
+ }
+
+ if(compareFn(this, _performanceStatsLevelSet.medium) <= 0)
+ {
+ return PerformanceRating.Medium;
+ }
+
+ if(compareFn(this, _performanceStatsLevelSet.poor) <= 0)
+ {
+ return PerformanceRating.Poor;
+ }
+
+ return PerformanceRating.VeryPoor;
+ }
+
+ private static bool ApproxLessOrEqual(float x1, float x2)
+ {
+ float r = x1 - x2;
+ return r < 0.0f || Mathf.Approximately(r, 0.0f);
+ }
+
+ #endregion
+
+ #region Overrides
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder sb = new System.Text.StringBuilder();
+ sb.AppendFormat("Avatar Name: {0}\n", avatarName);
+ sb.AppendFormat("Overall Performance: {0}\n", GetPerformanceRatingForCategory(AvatarPerformanceCategory.Overall));
+ sb.AppendFormat("Poly Count: {0}\n", polyCount);
+ sb.AppendFormat("Bounds: {0}\n", aabb.ToString());
+ sb.AppendFormat("Skinned Mesh Count: {0}\n", skinnedMeshCount);
+ sb.AppendFormat("Mesh Count: {0}\n", meshCount);
+ sb.AppendFormat("Material Count: {0}\n", materialCount);
+ sb.AppendFormat("Animator Count: {0}\n", animatorCount);
+ sb.AppendFormat("Bone Count: {0}\n", boneCount);
+ sb.AppendFormat("Light Count: {0}\n", lightCount);
+ sb.AppendFormat("Particle System Count: {0}\n", particleSystemCount);
+ sb.AppendFormat("Particle Total Count: {0}\n", particleTotalCount);
+ sb.AppendFormat("Particle Max Mesh Poly Count: {0}\n", particleMaxMeshPolyCount);
+ sb.AppendFormat("Particle Trails Enabled: {0}\n", particleTrailsEnabled);
+ sb.AppendFormat("Particle Collision Enabled: {0}\n", particleCollisionEnabled);
+ sb.AppendFormat("Trail Renderer Count: {0}\n", trailRendererCount);
+ sb.AppendFormat("Line Renderer Count: {0}\n", lineRendererCount);
+ sb.AppendFormat("Dynamic Bone Component Count: {0}\n", dynamicBoneComponentCount);
+ sb.AppendFormat("Dynamic Bone Simulated Bone Count: {0}\n", dynamicBoneSimulatedBoneCount);
+ sb.AppendFormat("Dynamic Bone Collider Count: {0}\n", dynamicBoneColliderCount);
+ sb.AppendFormat("Dynamic Bone Collision Check Count: {0}\n", dynamicBoneCollisionCheckCount);
+ sb.AppendFormat("Cloth Count: {0}\n", clothCount);
+ sb.AppendFormat("Cloth Max Vertices: {0}\n", clothMaxVertices);
+ sb.AppendFormat("Physics Collider Count: {0}\n", physicsColliderCount);
+ sb.AppendFormat("Physics Rigidbody Count: {0}\n", physicsRigidbodyCount);
+ if(downloadSize > 0)
+ {
+ sb.AppendFormat("Download Size: {0} MB\n", downloadSize);
+ }
+
+ return sb.ToString();
+ }
+
+ // Mirror the AvatarPerformanceStats class even if some aren't used right now.
+ [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
+ [SuppressMessage("ReSharper", "NotAccessedField.Global")]
+ public readonly struct Snapshot
+ {
+ public readonly string avatarName;
+
+ // Stats
+ public readonly int? polyCount;
+ public readonly Bounds? aabb;
+ public readonly int? skinnedMeshCount;
+ public readonly int? meshCount;
+ public readonly int? materialCount;
+ public readonly int? animatorCount;
+ public readonly int? boneCount;
+ public readonly int? lightCount;
+ public readonly int? particleSystemCount;
+ public readonly int? particleTotalCount;
+ public readonly int? particleMaxMeshPolyCount;
+ public readonly bool? particleTrailsEnabled;
+ public readonly bool? particleCollisionEnabled;
+ public readonly int? trailRendererCount;
+ public readonly int? lineRendererCount;
+ public readonly int? dynamicBoneComponentCount;
+ public readonly int? dynamicBoneSimulatedBoneCount;
+ public readonly int? dynamicBoneColliderCount;
+ public readonly int? dynamicBoneCollisionCheckCount; // number of collider simulated bones excluding the root multiplied by the number of colliders
+ public readonly int? clothCount;
+ public readonly int? clothMaxVertices;
+ public readonly int? physicsColliderCount;
+ public readonly int? physicsRigidbodyCount;
+ public readonly int? audioSourceCount;
+ public readonly float? downloadSize;
+
+ // Ratings
+ public readonly PerformanceRating overallRating;
+ public readonly PerformanceRating polyCountRating;
+ public readonly PerformanceRating aabbRating;
+ public readonly PerformanceRating skinnedMeshCountRating;
+ public readonly PerformanceRating meshCountRating;
+ public readonly PerformanceRating materialCountRating;
+ public readonly PerformanceRating animatorCountRating;
+ public readonly PerformanceRating boneCountRating;
+ public readonly PerformanceRating lightCountRating;
+ public readonly PerformanceRating particleSystemCountRating;
+ public readonly PerformanceRating particleTotalCountRating;
+ public readonly PerformanceRating particleMaxMeshPolyCountRating;
+ public readonly PerformanceRating particleTrailsEnabledRating;
+ public readonly PerformanceRating particleCollisionEnabledRating;
+ public readonly PerformanceRating trailRendererCountRating;
+ public readonly PerformanceRating lineRendererCountRating;
+ public readonly PerformanceRating dynamicBoneComponentCountRating;
+ public readonly PerformanceRating dynamicBoneSimulatedBoneCountRating;
+ public readonly PerformanceRating dynamicBoneColliderCountRating;
+ public readonly PerformanceRating dynamicBoneCollisionCheckCountRating;
+ public readonly PerformanceRating clothCountRating;
+ public readonly PerformanceRating clothMaxVerticesRating;
+ public readonly PerformanceRating physicsColliderCountRating;
+ public readonly PerformanceRating physicsRigidbodyCountRating;
+ public readonly PerformanceRating audioSourceCountRating;
+ public readonly PerformanceRating downloadSizeRating;
+
+ public Snapshot(AvatarPerformanceStats avatarPerformanceStats)
+ {
+ avatarName = avatarPerformanceStats.avatarName;
+ polyCount = avatarPerformanceStats.polyCount;
+ aabb = avatarPerformanceStats.aabb;
+ skinnedMeshCount = avatarPerformanceStats.skinnedMeshCount;
+ meshCount = avatarPerformanceStats.meshCount;
+ materialCount = avatarPerformanceStats.materialCount;
+ animatorCount = avatarPerformanceStats.animatorCount;
+ boneCount = avatarPerformanceStats.boneCount;
+ lightCount = avatarPerformanceStats.lightCount;
+ particleSystemCount = avatarPerformanceStats.particleSystemCount;
+ particleTotalCount = avatarPerformanceStats.particleTotalCount;
+ particleMaxMeshPolyCount = avatarPerformanceStats.particleMaxMeshPolyCount;
+ particleTrailsEnabled = avatarPerformanceStats.particleTrailsEnabled;
+ particleCollisionEnabled = avatarPerformanceStats.particleCollisionEnabled;
+ trailRendererCount = avatarPerformanceStats.trailRendererCount;
+ lineRendererCount = avatarPerformanceStats.lineRendererCount;
+ dynamicBoneComponentCount = avatarPerformanceStats.dynamicBoneComponentCount;
+ dynamicBoneSimulatedBoneCount = avatarPerformanceStats.dynamicBoneSimulatedBoneCount;
+ dynamicBoneColliderCount = avatarPerformanceStats.dynamicBoneColliderCount;
+ dynamicBoneCollisionCheckCount = avatarPerformanceStats.dynamicBoneCollisionCheckCount;
+ clothCount = avatarPerformanceStats.clothCount;
+ clothMaxVertices = avatarPerformanceStats.clothMaxVertices;
+ physicsColliderCount = avatarPerformanceStats.physicsColliderCount;
+ physicsRigidbodyCount = avatarPerformanceStats.physicsRigidbodyCount;
+ audioSourceCount = avatarPerformanceStats.audioSourceCount;
+ downloadSize = avatarPerformanceStats.downloadSize;
+
+ overallRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.Overall);
+ polyCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.PolyCount);
+ aabbRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.AABB);
+ skinnedMeshCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.SkinnedMeshCount);
+ meshCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.MeshCount);
+ materialCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.MaterialCount);
+ animatorCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.AnimatorCount);
+ boneCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.BoneCount);
+ lightCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.LightCount);
+ particleSystemCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ParticleSystemCount);
+ particleTotalCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ParticleTotalCount);
+ particleMaxMeshPolyCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ParticleMaxMeshPolyCount);
+ particleTrailsEnabledRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ParticleTrailsEnabled);
+ particleCollisionEnabledRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ParticleCollisionEnabled);
+ trailRendererCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.TrailRendererCount);
+ lineRendererCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.LineRendererCount);
+ dynamicBoneComponentCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.DynamicBoneComponentCount);
+ dynamicBoneSimulatedBoneCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.DynamicBoneSimulatedBoneCount);
+ dynamicBoneColliderCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.DynamicBoneColliderCount);
+ dynamicBoneCollisionCheckCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.DynamicBoneCollisionCheckCount);
+ clothCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ClothCount);
+ clothMaxVerticesRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.ClothMaxVertices);
+ physicsColliderCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.PhysicsColliderCount);
+ physicsRigidbodyCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.PhysicsRigidbodyCount);
+ audioSourceCountRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.AudioSourceCount);
+ downloadSizeRating = avatarPerformanceStats.GetPerformanceRatingForCategory(AvatarPerformanceCategory.DownloadSize);
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder sb = new System.Text.StringBuilder(1024);
+ sb.AppendFormat("Avatar Name: {0}\n", avatarName);
+ sb.AppendFormat("Overall Performance: {0}\n", overallRating);
+ sb.AppendFormat("Poly Count: {0}\n", polyCount);
+ sb.AppendFormat("Bounds: {0}\n", aabb.ToString());
+ sb.AppendFormat("Skinned Mesh Count: {0}\n", skinnedMeshCount);
+ sb.AppendFormat("Mesh Count: {0}\n", meshCount);
+ sb.AppendFormat("Material Count: {0}\n", materialCount);
+ sb.AppendFormat("Animator Count: {0}\n", animatorCount);
+ sb.AppendFormat("Bone Count: {0}\n", boneCount);
+ sb.AppendFormat("Light Count: {0}\n", lightCount);
+ sb.AppendFormat("Particle System Count: {0}\n", particleSystemCount);
+ sb.AppendFormat("Particle Total Count: {0}\n", particleTotalCount);
+ sb.AppendFormat("Particle Max Mesh Poly Count: {0}\n", particleMaxMeshPolyCount);
+ sb.AppendFormat("Particle Trails Enabled: {0}\n", particleTrailsEnabled);
+ sb.AppendFormat("Particle Collision Enabled: {0}\n", particleCollisionEnabled);
+ sb.AppendFormat("Trail Renderer Count: {0}\n", trailRendererCount);
+ sb.AppendFormat("Line Renderer Count: {0}\n", lineRendererCount);
+ sb.AppendFormat("Dynamic Bone Component Count: {0}\n", dynamicBoneComponentCount);
+ sb.AppendFormat("Dynamic Bone Simulated Bone Count: {0}\n", dynamicBoneSimulatedBoneCount);
+ sb.AppendFormat("Dynamic Bone Collider Count: {0}\n", dynamicBoneColliderCount);
+ sb.AppendFormat("Dynamic Bone Collision Check Count: {0}\n", dynamicBoneCollisionCheckCount);
+ sb.AppendFormat("Cloth Count: {0}\n", clothCount);
+ sb.AppendFormat("Cloth Max Vertices: {0}\n", clothMaxVertices);
+ sb.AppendFormat("Physics Collider Count: {0}\n", physicsColliderCount);
+ sb.AppendFormat("Physics Rigidbody Count: {0}\n", physicsRigidbodyCount);
+ sb.AppendFormat("Download Size: {0} MB\n", downloadSize);
+
+ return sb.ToString();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs.meta
new file mode 100644
index 00000000..894716a6
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStats.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1bf4fb79a49d4b109c4dce6b38e5548e
+timeCreated: 1561267926 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs
new file mode 100644
index 00000000..c31b43a2
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs
@@ -0,0 +1,32 @@
+using UnityEngine;
+
+namespace VRC.SDKBase.Validation.Performance.Stats
+{
+ public class AvatarPerformanceStatsLevel : ScriptableObject
+ {
+ public int polyCount;
+ public Bounds aabb;
+ public int skinnedMeshCount;
+ public int meshCount;
+ public int materialCount;
+ public int animatorCount;
+ public int boneCount;
+ public int lightCount;
+ public int particleSystemCount;
+ public int particleTotalCount;
+ public int particleMaxMeshPolyCount;
+ public bool particleTrailsEnabled;
+ public bool particleCollisionEnabled;
+ public int trailRendererCount;
+ public int lineRendererCount;
+ public int dynamicBoneComponentCount;
+ public int dynamicBoneSimulatedBoneCount;
+ public int dynamicBoneColliderCount;
+ public int dynamicBoneCollisionCheckCount;
+ public int clothCount;
+ public int clothMaxVertices;
+ public int physicsColliderCount;
+ public int physicsRigidbodyCount;
+ public int audioSourceCount;
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs.meta
new file mode 100644
index 00000000..966fbdc0
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f742c36dce5730f4d96e37d82c330584
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs
new file mode 100644
index 00000000..6490496b
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs
@@ -0,0 +1,15 @@
+using UnityEngine;
+using UnityEngine.Serialization;
+
+namespace VRC.SDKBase.Validation.Performance.Stats
+{
+ public class AvatarPerformanceStatsLevelSet : ScriptableObject
+ {
+ [FormerlySerializedAs("veryGood")]
+ public AvatarPerformanceStatsLevel excellent;
+ public AvatarPerformanceStatsLevel good;
+ public AvatarPerformanceStatsLevel medium;
+ [FormerlySerializedAs("bad")]
+ public AvatarPerformanceStatsLevel poor;
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs.meta
new file mode 100644
index 00000000..603c0a80
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/Performance/Stats/AvatarPerformanceStatsLevelSet.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 468554b1bfc447f41a20a2f5bae65d16
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs
new file mode 100644
index 00000000..0a69a1af
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs
@@ -0,0 +1,86 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace VRC.SDKBase.Validation
+{
+ public static class ShaderValidation
+ {
+ public static IEnumerable<Shader> FindIllegalShaders(GameObject target, string[] whitelist)
+ {
+ List<Shader> illegalShaders = new List<Shader>();
+ IEnumerator seeker = FindIllegalShadersEnumerator(target, whitelist, (c) => illegalShaders.Add(c));
+ while(seeker.MoveNext())
+ {
+ }
+
+ return illegalShaders;
+ }
+
+ private static IEnumerator FindIllegalShadersEnumerator(GameObject target, string[] whitelist, System.Action<Shader> onFound, bool useWatch = false)
+ {
+ System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
+ if(useWatch)
+ {
+ watch.Start();
+ }
+
+ List<Material> materialCache = new List<Material>();
+ Queue<GameObject> children = new Queue<GameObject>();
+ children.Enqueue(target.gameObject);
+ while(children.Count > 0)
+ {
+ GameObject child = children.Dequeue();
+ if(child == null)
+ {
+ continue;
+ }
+
+ for(int idx = 0; idx < child.transform.childCount; ++idx)
+ {
+ children.Enqueue(child.transform.GetChild(idx).gameObject);
+ }
+
+ foreach(Renderer childRenderers in child.transform.GetComponents<Renderer>())
+ {
+ if(childRenderers == null)
+ {
+ continue;
+ }
+
+ foreach(Material sharedMaterial in childRenderers.sharedMaterials)
+ {
+ if(materialCache.Any(cacheMtl => sharedMaterial == cacheMtl)) // did we already look at this one?
+ {
+ continue;
+ }
+
+ // Skip empty material slots, or materials without shaders.
+ // Both will end up using the magenta error shader.
+ if(sharedMaterial == null || sharedMaterial.shader == null)
+ {
+ continue;
+ }
+
+ if(whitelist.All(okayShaderName => sharedMaterial.shader.name != okayShaderName))
+ {
+ onFound(sharedMaterial.shader);
+ yield return null;
+ }
+
+ materialCache.Add(sharedMaterial);
+ }
+
+ if(!useWatch || watch.ElapsedMilliseconds <= 1)
+ {
+ continue;
+ }
+
+ yield return null;
+ watch.Reset();
+ }
+ }
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs.meta
new file mode 100644
index 00000000..1b9e8680
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ShaderValidation.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: bef0a8d1d2c547119a62b7d7a5c512ea
+timeCreated: 1563940360 \ No newline at end of file
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs
new file mode 100644
index 00000000..e6dfef95
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using UnityEngine;
+using System.Reflection;
+
+namespace VRC.SDKBase.Validation
+{
+ public static class ValidationUtils
+ {
+ public static void RemoveIllegalComponents(GameObject target, HashSet<Type> whitelist, bool retry = true, bool onlySceneObjects = false, bool logStripping = true)
+ {
+ List<Component> foundComponents = FindIllegalComponents(target, whitelist);
+ foreach(Component component in foundComponents)
+ {
+ if(component == null)
+ {
+ continue;
+ }
+
+ if(onlySceneObjects && component.GetInstanceID() < 0)
+ {
+ continue;
+ }
+
+ if(logStripping)
+ {
+ Core.Logger.LogWarning($"Removing {component.GetType().Name} comp from {component.gameObject.name}");
+ }
+
+ RemoveComponent(component);
+ }
+ }
+
+ public static List<Component> FindIllegalComponents(GameObject target, HashSet<Type> whitelist)
+ {
+ List<Component> foundComponents = new List<Component>();
+ Component[] allComponents = target.GetComponentsInChildren<Component>(true);
+ foreach(Component component in allComponents)
+ {
+ if(component == null)
+ {
+ continue;
+ }
+
+ Type componentType = component.GetType();
+ if(whitelist.Contains(componentType))
+ {
+ continue;
+ }
+
+ foundComponents.Add(component);
+ }
+
+ return foundComponents;
+ }
+
+ private static readonly Dictionary<string, Type> _typeCache = new Dictionary<string, Type>();
+ private static readonly Dictionary<string, HashSet<Type>> _whitelistCache = new Dictionary<string, HashSet<Type>>();
+ public static HashSet<Type> WhitelistedTypes(string whitelistName, IEnumerable<string> componentTypeWhitelist)
+ {
+ if (_whitelistCache.ContainsKey(whitelistName))
+ {
+ return _whitelistCache[whitelistName];
+ }
+
+ Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ HashSet<Type> whitelist = new HashSet<Type>();
+ foreach(string whitelistedTypeName in componentTypeWhitelist)
+ {
+ Type whitelistedType = GetTypeFromName(whitelistedTypeName, assemblies);
+ if(whitelistedType == null)
+ {
+ continue;
+ }
+
+ if(whitelist.Contains(whitelistedType))
+ {
+ continue;
+ }
+
+ whitelist.Add(whitelistedType);
+ }
+
+ AddDerivedClasses(whitelist);
+
+ _whitelistCache[whitelistName] = whitelist;
+
+ return _whitelistCache[whitelistName];
+ }
+
+ public static HashSet<Type> WhitelistedTypes(string whitelistName, IEnumerable<Type> componentTypeWhitelist)
+ {
+ if (_whitelistCache.ContainsKey(whitelistName))
+ {
+ return _whitelistCache[whitelistName];
+ }
+
+ HashSet<Type> whitelist = new HashSet<Type>();
+ whitelist.UnionWith(componentTypeWhitelist);
+
+ AddDerivedClasses(whitelist);
+
+ _whitelistCache[whitelistName] = whitelist;
+
+ return _whitelistCache[whitelistName];
+ }
+
+ private static void AddDerivedClasses(HashSet<Type> whitelist)
+ {
+ Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ foreach(Assembly assembly in assemblies)
+ {
+ foreach(Type type in assembly.GetTypes())
+ {
+ if(whitelist.Contains(type))
+ {
+ continue;
+ }
+
+ if(!typeof(Component).IsAssignableFrom(type))
+ {
+ continue;
+ }
+
+ Type currentType = type;
+ while(currentType != typeof(object) && currentType != null)
+ {
+ if(whitelist.Contains(currentType))
+ {
+ whitelist.Add(type);
+ break;
+ }
+
+ currentType = currentType.BaseType;
+ }
+ }
+ }
+ }
+
+ public static Type GetTypeFromName(string name, Assembly[] assemblies = null)
+ {
+ if (_typeCache.ContainsKey(name))
+ {
+
+ return _typeCache[name];
+ }
+
+ if (assemblies == null)
+ {
+ assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ }
+
+ foreach (Assembly assembly in assemblies)
+ {
+ Type found = assembly.GetType(name);
+ if(found == null)
+ {
+ continue;
+ }
+
+ _typeCache[name] = found;
+ return found;
+ }
+
+ //This is really verbose for some SDK scenes, eg.
+ //If they don't have FinalIK installed
+#if VRC_CLIENT && UNITY_EDITOR
+ Debug.LogWarningFormat("Could not find type {0}", name);
+#endif
+
+ _typeCache[name] = null;
+ return null;
+ }
+
+ private static readonly Dictionary<Type, ImmutableArray<RequireComponent>> _requireComponentsCache = new Dictionary<Type, ImmutableArray<RequireComponent>>();
+ private static void RemoveDependencies(Component rootComponent)
+ {
+ if (rootComponent == null)
+ {
+ return;
+ }
+
+ Component[] components = rootComponent.GetComponents<Component>();
+ if (components == null || components.Length == 0)
+ {
+ return;
+ }
+
+ Type compType = rootComponent.GetType();
+ foreach (var siblingComponent in components)
+ {
+ if (siblingComponent == null)
+ {
+ continue;
+ }
+
+ Type siblingComponentType = siblingComponent.GetType();
+ if(!_requireComponentsCache.TryGetValue(siblingComponentType, out ImmutableArray<RequireComponent> requiredComponentAttributes))
+ {
+ requiredComponentAttributes = siblingComponentType.GetCustomAttributes(typeof(RequireComponent), true).Cast<RequireComponent>().ToImmutableArray();
+ _requireComponentsCache.Add(siblingComponentType, requiredComponentAttributes);
+ }
+
+ bool deleteMe = false;
+ foreach (RequireComponent requireComponent in requiredComponentAttributes)
+ {
+ if (requireComponent == null)
+ {
+ continue;
+ }
+
+ if(requireComponent.m_Type0 != compType && requireComponent.m_Type1 != compType && requireComponent.m_Type2 != compType)
+ {
+ continue;
+ }
+
+ deleteMe = true;
+ break;
+ }
+
+ if (deleteMe && siblingComponent != rootComponent)
+ {
+ RemoveComponent(siblingComponent);
+ }
+ }
+ }
+
+ public static void RemoveComponent(Component comp)
+ {
+ if (comp == null)
+ {
+ return;
+ }
+
+ RemoveDependencies(comp);
+
+#if VRC_CLIENT
+ UnityEngine.Object.DestroyImmediate(comp, true);
+#else
+ UnityEngine.Object.DestroyImmediate(comp, false);
+#endif
+ }
+
+ public static void RemoveComponentsOfType<T>(GameObject target) where T : Component
+ {
+ if (target == null)
+ return;
+
+ foreach (T comp in target.GetComponentsInChildren<T>(true))
+ {
+ if (comp == null || comp.gameObject == null)
+ continue;
+
+ RemoveComponent(comp);
+ }
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs.meta
new file mode 100644
index 00000000..adc2f728
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/ValidationUtils.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 8a90ec11b51863c4cb2d8a8cee31c2fb
+timeCreated: 1504829091
+licenseType: Pro
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs
new file mode 100644
index 00000000..3dab42c2
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs
@@ -0,0 +1,682 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+#if TextMeshPro
+using TMPro;
+#endif
+using UnityEngine;
+using UnityEngine.Playables;
+using UnityEngine.Timeline;
+using UnityEngine.UI;
+
+namespace VRC.SDKBase.Validation
+{
+ public static class WorldValidation
+ {
+ private static readonly Lazy<int> _debugLevel = new Lazy<int>(InitializeLogging);
+ private static int DebugLevel => _debugLevel.Value;
+
+ private static int InitializeLogging()
+ {
+ int hashCode = typeof(WorldValidation).GetHashCode();
+ VRC.Core.Logger.DescribeDebugLevel(hashCode, "WorldValidation", VRC.Core.Logger.Color.red);
+ VRC.Core.Logger.AddDebugLevel(hashCode);
+ return hashCode;
+ }
+
+ static string[] ComponentTypeWhiteList = null;
+
+ public enum WhiteListConfiguration
+ {
+ None,
+ VRCSDK2,
+ VRCSDK3,
+ Unchanged
+ }
+
+ static WhiteListConfiguration ComponentTypeWhiteListConfiguration = WhiteListConfiguration.None;
+
+ static readonly string[] ComponentTypeWhiteListCommon = new string[]
+ {
+ #if UNITY_STANDALONE
+ "UnityEngine.Rendering.PostProcessing.PostProcessDebug",
+ "UnityEngine.Rendering.PostProcessing.PostProcessLayer",
+ "UnityEngine.Rendering.PostProcessing.PostProcessVolume",
+ #endif
+ "VRC.Core.PipelineManager",
+ "UiInputField",
+ "VRCProjectSettings",
+ "DynamicBone",
+ "DynamicBoneCollider",
+ "TMPro.TMP_Dropdown",
+ "TMPro.TMP_InputField",
+ "TMPro.TMP_ScrollbarEventHandler",
+ "TMPro.TMP_SelectionCaret",
+ "TMPro.TMP_SpriteAnimator",
+ "TMPro.TMP_SubMesh",
+ "TMPro.TMP_SubMeshUI",
+ "TMPro.TMP_Text",
+ "TMPro.TextMeshPro",
+ "TMPro.TextMeshProUGUI",
+ "TMPro.TextContainer",
+ "TMPro.TMP_Dropdown+DropdownItem",
+ "UnityEngine.EventSystems.EventSystem",
+ "UnityEngine.EventSystems.EventTrigger",
+ "UnityEngine.EventSystems.UIBehaviour",
+ "UnityEngine.EventSystems.BaseInput",
+ "UnityEngine.EventSystems.BaseInputModule",
+ "UnityEngine.EventSystems.PointerInputModule",
+ "UnityEngine.EventSystems.StandaloneInputModule",
+ "UnityEngine.EventSystems.TouchInputModule",
+ "UnityEngine.EventSystems.BaseRaycaster",
+ "UnityEngine.EventSystems.PhysicsRaycaster",
+ "UnityEngine.UI.Button",
+ "UnityEngine.UI.Dropdown",
+ "UnityEngine.UI.Dropdown+DropdownItem",
+ "UnityEngine.UI.Graphic",
+ "UnityEngine.UI.GraphicRaycaster",
+ "UnityEngine.UI.Image",
+ "UnityEngine.UI.InputField",
+ "UnityEngine.UI.Mask",
+ "UnityEngine.UI.MaskableGraphic",
+ "UnityEngine.UI.RawImage",
+ "UnityEngine.UI.RectMask2D",
+ "UnityEngine.UI.Scrollbar",
+ "UnityEngine.UI.ScrollRect",
+ "UnityEngine.UI.Selectable",
+ "UnityEngine.UI.Slider",
+ "UnityEngine.UI.Text",
+ "UnityEngine.UI.Toggle",
+ "UnityEngine.UI.ToggleGroup",
+ "UnityEngine.UI.AspectRatioFitter",
+ "UnityEngine.UI.CanvasScaler",
+ "UnityEngine.UI.ContentSizeFitter",
+ "UnityEngine.UI.GridLayoutGroup",
+ "UnityEngine.UI.HorizontalLayoutGroup",
+ "UnityEngine.UI.HorizontalOrVerticalLayoutGroup",
+ "UnityEngine.UI.LayoutElement",
+ "UnityEngine.UI.LayoutGroup",
+ "UnityEngine.UI.VerticalLayoutGroup",
+ "UnityEngine.UI.BaseMeshEffect",
+ "UnityEngine.UI.Outline",
+ "UnityEngine.UI.PositionAsUV1",
+ "UnityEngine.UI.Shadow",
+ "OVRLipSync",
+ "OVRLipSyncContext",
+ "OVRLipSyncContextBase",
+ "OVRLipSyncContextCanned",
+ "OVRLipSyncContextMorphTarget",
+ "OVRLipSyncContextTextureFlip",
+ "ONSPReflectionZone",
+ "OculusSpatializerUnity",
+ "ONSPAmbisonicsNative",
+ "ONSPAudioSource",
+ "RootMotion.FinalIK.BipedIK",
+ "RootMotion.FinalIK.FingerRig",
+ "RootMotion.FinalIK.Grounder",
+ "RootMotion.FinalIK.GrounderBipedIK",
+ "RootMotion.FinalIK.GrounderFBBIK",
+ "RootMotion.FinalIK.GrounderIK",
+ "RootMotion.FinalIK.GrounderQuadruped",
+ "RootMotion.FinalIK.GrounderVRIK",
+ "RootMotion.FinalIK.AimIK",
+ "RootMotion.FinalIK.CCDIK",
+ "RootMotion.FinalIK.FABRIK",
+ "RootMotion.FinalIK.FABRIKRoot",
+ "RootMotion.FinalIK.FullBodyBipedIK",
+ "RootMotion.FinalIK.IK",
+ "RootMotion.FinalIK.IKExecutionOrder",
+ "RootMotion.FinalIK.LegIK",
+ "RootMotion.FinalIK.LimbIK",
+ "RootMotion.FinalIK.LookAtIK",
+ "RootMotion.FinalIK.TrigonometricIK",
+ "RootMotion.FinalIK.VRIK",
+ "RootMotion.FinalIK.FBBIKArmBending",
+ "RootMotion.FinalIK.FBBIKHeadEffector",
+ "RootMotion.FinalIK.TwistRelaxer",
+ "RootMotion.FinalIK.InteractionObject",
+ "RootMotion.FinalIK.InteractionSystem",
+ "RootMotion.FinalIK.InteractionTarget",
+ "RootMotion.FinalIK.InteractionTrigger",
+ "RootMotion.FinalIK.GenericPoser",
+ "RootMotion.FinalIK.HandPoser",
+ "RootMotion.FinalIK.Poser",
+ "RootMotion.FinalIK.RagdollUtility",
+ "RootMotion.FinalIK.RotationLimit",
+ "RootMotion.FinalIK.RotationLimitAngle",
+ "RootMotion.FinalIK.RotationLimitHinge",
+ "RootMotion.FinalIK.RotationLimitPolygonal",
+ "RootMotion.FinalIK.RotationLimitSpline",
+ "RootMotion.FinalIK.AimPoser",
+ "RootMotion.FinalIK.Amplifier",
+ "RootMotion.FinalIK.BodyTilt",
+ "RootMotion.FinalIK.HitReaction",
+ "RootMotion.FinalIK.HitReactionVRIK",
+ "RootMotion.FinalIK.Inertia",
+ "RootMotion.FinalIK.OffsetModifier",
+ "RootMotion.FinalIK.OffsetModifierVRIK",
+ "RootMotion.FinalIK.OffsetPose",
+ "RootMotion.FinalIK.Recoil",
+ "RootMotion.FinalIK.ShoulderRotator",
+ "RootMotion.Dynamics.AnimationBlocker",
+ "RootMotion.Dynamics.BehaviourBase",
+ "RootMotion.Dynamics.BehaviourFall",
+ "RootMotion.Dynamics.BehaviourPuppet",
+ "RootMotion.Dynamics.JointBreakBroadcaster",
+ "RootMotion.Dynamics.MuscleCollisionBroadcaster",
+ "RootMotion.Dynamics.PressureSensor",
+ "RootMotion.Dynamics.Prop",
+ "RootMotion.Dynamics.PropRoot",
+ "RootMotion.Dynamics.PuppetMaster",
+ "RootMotion.Dynamics.PuppetMasterSettings",
+ // TODO: remove these if they are only needed in editor
+ "RootMotion.Dynamics.BipedRagdollCreator",
+ "RootMotion.Dynamics.RagdollCreator",
+ "RootMotion.Dynamics.RagdollEditor",
+ //
+ "RootMotion.SolverManager",
+ "RootMotion.TriggerEventBroadcaster",
+ "UnityEngine.WindZone",
+ "UnityEngine.Tilemaps.Tilemap",
+ "UnityEngine.Tilemaps.TilemapRenderer",
+ "UnityEngine.Terrain",
+ "UnityEngine.Tree",
+ "UnityEngine.SpriteMask",
+ "UnityEngine.Grid",
+ "UnityEngine.GridLayout",
+ "UnityEngine.AudioSource",
+ "UnityEngine.AudioReverbZone",
+ "UnityEngine.AudioLowPassFilter",
+ "UnityEngine.AudioHighPassFilter",
+ "UnityEngine.AudioDistortionFilter",
+ "UnityEngine.AudioEchoFilter",
+ "UnityEngine.AudioChorusFilter",
+ "UnityEngine.AudioReverbFilter",
+ "UnityEngine.Playables.PlayableDirector",
+ "UnityEngine.TerrainCollider",
+ "UnityEngine.Canvas",
+ "UnityEngine.CanvasGroup",
+ "UnityEngine.CanvasRenderer",
+ "UnityEngine.TextMesh",
+ "UnityEngine.Animator",
+ "UnityEngine.AI.NavMeshAgent",
+ "UnityEngine.AI.NavMeshObstacle",
+ "UnityEngine.AI.OffMeshLink",
+ "UnityEngine.Cloth",
+ "UnityEngine.WheelCollider",
+ "UnityEngine.Rigidbody",
+ "UnityEngine.Joint",
+ "UnityEngine.HingeJoint",
+ "UnityEngine.SpringJoint",
+ "UnityEngine.FixedJoint",
+ "UnityEngine.CharacterJoint",
+ "UnityEngine.ConfigurableJoint",
+ "UnityEngine.ConstantForce",
+ "UnityEngine.Collider",
+ "UnityEngine.BoxCollider",
+ "UnityEngine.SphereCollider",
+ "UnityEngine.MeshCollider",
+ "UnityEngine.CapsuleCollider",
+ "UnityEngine.CharacterController",
+ "UnityEngine.ParticleSystem",
+ "UnityEngine.ParticleSystemRenderer",
+ "UnityEngine.BillboardRenderer",
+ "UnityEngine.Camera",
+ "UnityEngine.FlareLayer",
+ "UnityEngine.SkinnedMeshRenderer",
+ "UnityEngine.Renderer",
+ "UnityEngine.TrailRenderer",
+ "UnityEngine.LineRenderer",
+ "UnityEngine.GUIElement",
+ "UnityEngine.GUILayer",
+ "UnityEngine.Light",
+ "UnityEngine.LightProbeGroup",
+ "UnityEngine.LightProbeProxyVolume",
+ "UnityEngine.LODGroup",
+ "UnityEngine.ReflectionProbe",
+ "UnityEngine.SpriteRenderer",
+ "UnityEngine.Transform",
+ "UnityEngine.RectTransform",
+ "UnityEngine.Rendering.SortingGroup",
+ "UnityEngine.Projector",
+ "UnityEngine.OcclusionPortal",
+ "UnityEngine.OcclusionArea",
+ "UnityEngine.LensFlare",
+ "UnityEngine.Skybox",
+ "UnityEngine.MeshFilter",
+ "UnityEngine.Halo",
+ "UnityEngine.MeshRenderer",
+ "UnityEngine.Collider2D",
+ "UnityEngine.Rigidbody2D",
+ "UnityEngine.CompositeCollider2D",
+ "UnityEngine.ConstantForce2D",
+ "UnityEngine.AreaEffector2D",
+ "UnityEngine.CapsuleCollider2D",
+ "UnityEngine.DistanceJoint2D",
+ "UnityEngine.EdgeCollider2D",
+ "UnityEngine.Effector2D",
+ "UnityEngine.BoxCollider2D",
+ "UnityEngine.CircleCollider2D",
+ "UnityEngine.FixedJoint2D",
+ "UnityEngine.HingeJoint2D",
+ "UnityEngine.FrictionJoint2D",
+ "UnityEngine.PlatformEffector2D",
+ "UnityEngine.PointEffector2D",
+ "UnityEngine.PolygonCollider2D",
+ "UnityEngine.SliderJoint2D",
+ "UnityEngine.SurfaceEffector2D",
+ "UnityEngine.RelativeJoint2D",
+ "UnityEngine.TargetJoint2D",
+ "UnityEngine.WheelJoint2D",
+ "UnityEngine.Joint2D",
+ "UnityEngine.ParticleSystemForceField"
+ };
+
+ static readonly string[] ComponentTypeWhiteListSdk2 = new string[]
+ {
+ #if UNITY_STANDALONE
+ "VRCSDK2.VRC_CustomRendererBehaviour",
+ "VRCSDK2.VRC_MidiNoteIn",
+ "VRCSDK2.scripts.Scenes.VRC_Panorama",
+ "VRCSDK2.VRC_Water",
+ "UnityStandardAssets.Water.WaterBasic",
+ "UnityStandardAssets.Water.Displace",
+ "UnityStandardAssets.Water.GerstnerDisplace",
+ "UnityStandardAssets.Water.PlanarReflection",
+ "UnityStandardAssets.Water.SpecularLighting",
+ "UnityStandardAssets.Water.Water",
+ "UnityStandardAssets.Water.WaterBase",
+ "UnityStandardAssets.Water.WaterTile",
+ #endif
+ "VRCSDK2.VRCTriggerRelay",
+ "VRCSDK2.VRC_AudioBank",
+ "VRCSDK2.VRC_DataStorage",
+ "VRCSDK2.VRC_EventHandler",
+ "VRCSDK2.VRC_IKFollower",
+ "VRCSDK2.VRC_Label",
+ "VRCSDK2.VRC_KeyEvents",
+ "VRCSDK2.VRC_PhysicsRoot",
+ "VRCSDK2.VRC_CombatSystem",
+ "VRCSDK2.VRC_DestructibleStandard",
+ "VRC_VisualDamage",
+ "VRCSDK2.VRC_OscButtonIn",
+ "VRCSDK2.VRC_GunStats",
+ "VRCSDK2.VRC_JukeBox",
+ "VRCSDK2.VRC_AddDamage",
+ "VRCSDK2.VRC_AddHealth",
+ "VRCSDK2.VRC_AvatarCalibrator",
+ "VRCSDK2.VRC_AvatarPedestal",
+ "VRCSDK2.VRC_NPCSpawn",
+ "VRCSDK2.VRC_ObjectSpawn",
+ "VRCSDK2.VRC_ObjectSync",
+ "VRCSDK2.VRC_Pickup",
+ "VRCSDK2.VRC_PortalMarker",
+ "VRCSDK2.VRC_SlideShow",
+ "VRCSDK2.VRC_SpatialAudioSource",
+ "VRCSDK2.VRC_StationInput",
+ "VRCSDK2.VRC_SyncAnimation",
+ "VRCSDK2.VRC_SyncVideoPlayer",
+ "VRCSDK2.VRC_SyncVideoStream",
+ "VRCSDK2.VRC_VideoScreen",
+ "VRCSDK2.VRC_VideoSpeaker",
+ "VRCSDK2.VRC_PlayerAudioOverride",
+ "VRCSDK2.VRC_MirrorReflection",
+ "VRCSDK2.VRC_PlayerMods",
+ "VRCSDK2.VRC_SceneDescriptor",
+ "VRCSDK2.VRC_SceneResetPosition",
+ "VRCSDK2.VRC_SceneSmoothShift",
+ "VRCSDK2.VRC_SpecialLayer",
+ "VRCSDK2.VRC_Station",
+ "VRCSDK2.VRC_StereoObject",
+ "VRCSDK2.VRC_TimedEvents",
+ "VRCSDK2.VRC_Trigger",
+ "VRCSDK2.VRC_TriggerColliderEventTrigger",
+ "VRCSDK2.VRC_UseEvents",
+ "VRCSDK2.VRC_UiShape",
+ "UnityEngine.Animation",
+ #if !UNITY_2019_4_OR_NEWER
+ "UnityEngine.GUIText",
+ "UnityEngine.GUITexture",
+ #endif
+ "UnityEngine.Video.VideoPlayer",
+ "PhysSound.PhysSoundBase",
+ "PhysSound.PhysSoundObject",
+ "PhysSound.PhysSoundTempAudio",
+ "PhysSound.PhysSoundTempAudioPool",
+ "PhysSound.PhysSoundTerrain",
+ "RealisticEyeMovements.EyeAndHeadAnimator",
+ "RealisticEyeMovements.LookTargetController",
+ "UnityStandardAssets.Cameras.AbstractTargetFollower",
+ "UnityStandardAssets.Cameras.AutoCam",
+ "UnityStandardAssets.Cameras.FreeLookCam",
+ "UnityStandardAssets.Cameras.HandHeldCam",
+ "UnityStandardAssets.Cameras.LookatTarget",
+ "UnityStandardAssets.Cameras.PivotBasedCameraRig",
+ "UnityStandardAssets.Cameras.ProtectCameraFromWallClip",
+ "UnityStandardAssets.Cameras.TargetFieldOfView",
+ "UnityStandardAssets.Characters.FirstPerson.FirstPersonController",
+ "UnityStandardAssets.Characters.FirstPerson.HeadBob",
+ "UnityStandardAssets.Characters.FirstPerson.RigidbodyFirstPersonController",
+ "UnityStandardAssets.Vehicles.Ball.Ball",
+ "UnityStandardAssets.Vehicles.Ball.BallUserControl",
+ "UnityStandardAssets.Characters.ThirdPerson.AICharacterControl",
+ "UnityStandardAssets.Characters.ThirdPerson.ThirdPersonCharacter",
+ "UnityStandardAssets.Characters.ThirdPerson.ThirdPersonUserControl",
+ "UnityStandardAssets.CrossPlatformInput.AxisTouchButton",
+ "UnityStandardAssets.CrossPlatformInput.ButtonHandler",
+ "UnityStandardAssets.CrossPlatformInput.InputAxisScrollbar",
+ "UnityStandardAssets.CrossPlatformInput.Joystick",
+ "UnityStandardAssets.CrossPlatformInput.MobileControlRig",
+ "UnityStandardAssets.CrossPlatformInput.TiltInput",
+ "UnityStandardAssets.CrossPlatformInput.TouchPad",
+ "UnityStandardAssets.Effects.AfterburnerPhysicsForce",
+ "UnityStandardAssets.Effects.ExplosionFireAndDebris",
+ "UnityStandardAssets.Effects.ExplosionPhysicsForce",
+ "UnityStandardAssets.Effects.Explosive",
+ "UnityStandardAssets.Effects.ExtinguishableParticleSystem",
+ "UnityStandardAssets.Effects.FireLight",
+ "UnityStandardAssets.Effects.Hose",
+ "UnityStandardAssets.Effects.ParticleSystemMultiplier",
+ "UnityStandardAssets.Effects.SmokeParticles",
+ "UnityStandardAssets.Effects.WaterHoseParticles",
+ "UnityStandardAssets.Utility.ActivateTrigger",
+ "UnityStandardAssets.Utility.AutoMoveAndRotate",
+ "UnityStandardAssets.Utility.DragRigidbody",
+ "UnityStandardAssets.Utility.DynamicShadowSettings",
+ "UnityStandardAssets.Utility.FollowTarget",
+ "UnityStandardAssets.Utility.FPSCounter",
+ "UnityStandardAssets.Utility.ObjectResetter",
+ "UnityStandardAssets.Utility.ParticleSystemDestroyer",
+ #if !UNITY_2019_4_OR_NEWER
+ "UnityStandardAssets.Utility.SimpleActivatorMenu",
+ #endif
+ "UnityStandardAssets.Utility.SimpleMouseRotator",
+ "UnityStandardAssets.Utility.SmoothFollow",
+ "UnityStandardAssets.Utility.TimedObjectActivator",
+ "UnityStandardAssets.Utility.TimedObjectDestructor",
+ "UnityStandardAssets.Utility.WaypointCircuit",
+ "UnityStandardAssets.Utility.WaypointProgressTracker",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplaneAiControl",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplaneAudio",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplaneController",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplaneControlSurfaceAnimator",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplanePropellerAnimator",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplaneUserControl2Axis",
+ "UnityStandardAssets.Vehicles.Aeroplane.AeroplaneUserControl4Axis",
+ "UnityStandardAssets.Vehicles.Aeroplane.JetParticleEffect",
+ "UnityStandardAssets.Vehicles.Aeroplane.LandingGear",
+ "UnityStandardAssets.Vehicles.Car.BrakeLight",
+ "UnityStandardAssets.Vehicles.Car.CarAIControl",
+ "UnityStandardAssets.Vehicles.Car.CarAudio",
+ "UnityStandardAssets.Vehicles.Car.CarController",
+ "UnityStandardAssets.Vehicles.Car.CarSelfRighting",
+ "UnityStandardAssets.Vehicles.Car.CarUserControl",
+ "UnityStandardAssets.Vehicles.Car.Mudguard",
+ "UnityStandardAssets.Vehicles.Car.SkidTrail",
+ "UnityStandardAssets.Vehicles.Car.Suspension",
+ "UnityStandardAssets.Vehicles.Car.WheelEffects",
+ "RenderHeads.Media.AVProVideo.ApplyToMaterial",
+ "RenderHeads.Media.AVProVideo.ApplyToMesh",
+ "RenderHeads.Media.AVProVideo.AudioOutput",
+ "RenderHeads.Media.AVProVideo.CubemapCube",
+ "RenderHeads.Media.AVProVideo.DebugOverlay",
+ "RenderHeads.Media.AVProVideo.DisplayBackground",
+ "RenderHeads.Media.AVProVideo.DisplayIMGUI",
+ "RenderHeads.Media.AVProVideo.DisplayUGUI",
+ "RenderHeads.Media.AVProVideo.MediaPlayer",
+ "RenderHeads.Media.AVProVideo.StreamParser",
+ "RenderHeads.Media.AVProVideo.SubtitlesUGUI",
+ "RenderHeads.Media.AVProVideo.UpdateStereoMaterial",
+ "AlphaButtonClickMask",
+ "EventSystemChecker",
+ "VirtualMarketplaceItem",
+ "SDK2UrlLauncher"
+ };
+
+ static readonly string[] ComponentTypeWhiteListSdk3 = new string[]
+ {
+ "VRC.SDK3.VRCDestructibleStandard",
+ "VRC.SDK3.Components.VRCVisualDamage",
+ "VRC.SDK3.Components.VRCAvatarPedestal",
+ "VRC.SDK3.Components.VRCPickup",
+ "VRC.SDK3.Components.VRCPortalMarker",
+ "VRC.SDK3.Components.VRCSpatialAudioSource",
+ "VRC.SDK3.Components.VRCMirrorReflection",
+ "VRC.SDK3.Components.VRCSceneDescriptor",
+ "VRC.SDK3.Components.VRCStation",
+ "VRC.SDK3.Components.VRCUiShape",
+ "VRC.SDK3.Components.VRCObjectSync",
+ "VRC.SDK3.Components.VRCObjectPool",
+ "VRC.SDK3.Video.Components.VRCUnityVideoPlayer",
+ "VRC.SDK3.Video.Components.AVPro.VRCAVProVideoPlayer",
+ "VRC.SDK3.Video.Components.AVPro.VRCAVProVideoScreen",
+ "VRC.SDK3.Video.Components.AVPro.VRCAVProVideoSpeaker",
+ "VRC.SDK3.Midi.VRCMidiListener",
+ "VRC.Udon.UdonBehaviour",
+ "VRC.Udon.AbstractUdonBehaviourEventProxy",
+ "UnityEngine.Animations.AimConstraint",
+ "UnityEngine.Animations.LookAtConstraint",
+ "UnityEngine.Animations.ParentConstraint",
+ "UnityEngine.Animations.PositionConstraint",
+ "UnityEngine.Animations.RotationConstraint",
+ "UnityEngine.Animations.ScaleConstraint",
+ "UnityEngine.ParticleSystemForceField",
+ "Cinemachine.Cinemachine3rdPersonAim",
+ "Cinemachine.CinemachineBlendListCamera",
+ "Cinemachine.CinemachineBrain",
+ "Cinemachine.CinemachineCameraOffset",
+ "Cinemachine.CinemachineClearShot",
+ "Cinemachine.CinemachineCollider",
+ "Cinemachine.CinemachineConfiner",
+ "Cinemachine.CinemachineDollyCart",
+ "Cinemachine.CinemachineExternalCamera",
+ "Cinemachine.CinemachineFollowZoom",
+ "Cinemachine.CinemachineFreeLook",
+ "Cinemachine.CinemachineMixingCamera",
+ "Cinemachine.CinemachinePath",
+ "Cinemachine.CinemachinePipeline",
+ "Cinemachine.CinemachinePixelPerfect",
+ "Cinemachine.CinemachineRecomposer",
+ "Cinemachine.CinemachineSmoothPath",
+ "Cinemachine.CinemachineStateDrivenCamera",
+ "Cinemachine.CinemachineStoryboard",
+ "Cinemachine.CinemachineTargetGroup",
+ "Cinemachine.CinemachineVirtualCamera",
+ "Cinemachine.Cinemachine3rdPersonFollow",
+ "Cinemachine.CinemachineBasicMultiChannelPerlin",
+ "Cinemachine.CinemachineComposer",
+ "Cinemachine.CinemachineFramingTransposer",
+ "Cinemachine.CinemachineGroupComposer",
+ "Cinemachine.CinemachineHardLockToTarget",
+ "Cinemachine.CinemachineHardLookAt",
+ "Cinemachine.CinemachineOrbitalTransposer",
+ "Cinemachine.CinemachinePOV",
+ "Cinemachine.CinemachineSameAsFollowTarget",
+ "Cinemachine.CinemachineTrackedDolly",
+ "Cinemachine.CinemachineTransposer",
+ "Cinemachine.CinemachineCore"
+ };
+
+ public static readonly string[] ShaderWhiteList = new string[]
+ {
+ "VRChat/Mobile/Standard Lite",
+ "VRChat/Mobile/Diffuse",
+ "VRChat/Mobile/Bumped Diffuse",
+ "VRChat/Mobile/Bumped Mapped Specular",
+ "VRChat/Mobile/Toon Lit",
+ "VRChat/Mobile/MatCap Lit",
+ "VRChat/Mobile/Lightmapped",
+ "VRChat/Mobile/Skybox",
+ "VRChat/Mobile/Particles/Additive",
+ "VRChat/Mobile/Particles/Multiply",
+ "FX/MirrorReflection",
+ "UI/Default",
+ };
+
+ private static readonly HashSet<int> scannedObjects = new HashSet<int>();
+
+ private static void ConfigureWhiteList(WhiteListConfiguration config)
+ {
+ if(ComponentTypeWhiteListConfiguration == config ||
+ config == WhiteListConfiguration.Unchanged)
+ {
+ return;
+ }
+
+ List<string> concatenation = new List<string>();
+ concatenation.AddRange(ComponentTypeWhiteListCommon);
+
+ switch(config)
+ {
+ case WhiteListConfiguration.VRCSDK2:
+ concatenation.AddRange(ComponentTypeWhiteListSdk2);
+ break;
+ case WhiteListConfiguration.VRCSDK3:
+ concatenation.AddRange(ComponentTypeWhiteListSdk3);
+ break;
+ }
+
+ ComponentTypeWhiteListConfiguration = config;
+ ComponentTypeWhiteList = concatenation.ToArray();
+ }
+
+ [PublicAPI]
+ public static void RemoveIllegalComponents(List<GameObject> targets, WhiteListConfiguration config, bool retry = true, HashSet<Type> tagWhitelistedTypes = null)
+ {
+ ConfigureWhiteList(config);
+
+ HashSet<Type> whitelist = ValidationUtils.WhitelistedTypes($"world{config}", ComponentTypeWhiteList);
+
+ // combine whitelist types from world tags with cached whitelist
+ if (tagWhitelistedTypes != null)
+ {
+ tagWhitelistedTypes.UnionWith(whitelist);
+ }
+
+ foreach(GameObject target in targets)
+ {
+ ValidationUtils.RemoveIllegalComponents(target, (tagWhitelistedTypes == null) ? whitelist : tagWhitelistedTypes, retry, true, true);
+ SecurityScan(target);
+ AddScanned(target);
+ }
+ }
+
+ private static void AddScanned(GameObject obj)
+ {
+ if(obj == null)
+ return;
+
+ if(!scannedObjects.Contains(obj.GetInstanceID()))
+ scannedObjects.Add(obj.GetInstanceID());
+
+ for(int idx = 0; idx < obj.transform.childCount; ++idx)
+ AddScanned(obj.transform.GetChild(idx)?.gameObject);
+ }
+
+ private static bool WasScanned(GameObject obj)
+ {
+ return scannedObjects.Contains(obj.GetInstanceID());
+ }
+
+ [PublicAPI]
+ public static void ScanGameObject(GameObject target, WhiteListConfiguration config)
+ {
+ if(WasScanned(target))
+ {
+ return;
+ }
+
+ ConfigureWhiteList(config);
+ HashSet<Type> whitelist = ValidationUtils.WhitelistedTypes("world" + config, ComponentTypeWhiteList);
+ ValidationUtils.RemoveIllegalComponents(target, whitelist);
+ SecurityScan(target);
+ AddScanned(target);
+
+ // Must be called after AddScanned to avoid infinite recursion.
+ ScanDropdownTemplates(target, config);
+ }
+
+ [PublicAPI]
+ public static void ClearScannedGameObjectCache()
+ {
+ scannedObjects.Clear();
+ }
+
+ [PublicAPI]
+ public static IEnumerable<Shader> FindIllegalShaders(GameObject target)
+ {
+ return ShaderValidation.FindIllegalShaders(target, ShaderWhiteList);
+ }
+
+ private static void SecurityScan(GameObject target)
+ {
+ PlayableDirector[] playableDirectors = target.GetComponentsInChildren<PlayableDirector>(true);
+ foreach(PlayableDirector playableDirector in playableDirectors)
+ {
+ StripPlayableDirectorWithPrefabs(playableDirector);
+ }
+ }
+
+ private static void ScanDropdownTemplates(GameObject target, WhiteListConfiguration config)
+ {
+ Dropdown[] dropdowns = target.GetComponentsInChildren<Dropdown>(true);
+ foreach(Dropdown dropdown in dropdowns)
+ {
+ if(dropdown == null)
+ {
+ continue;
+ }
+
+ RectTransform dropdownTemplate = dropdown.template;
+ if(dropdownTemplate == null)
+ {
+ continue;
+ }
+
+ ScanGameObject(dropdownTemplate.transform.root.gameObject, config);
+ }
+
+ #if TextMeshPro
+ TMP_Dropdown[] tmpDropdowns = target.GetComponentsInChildren<TMP_Dropdown>(true);
+ foreach(TMP_Dropdown textMeshProDropdown in tmpDropdowns)
+ {
+ if(textMeshProDropdown == null)
+ {
+ continue;
+ }
+
+ RectTransform dropdownTemplate = textMeshProDropdown.template;
+ if(dropdownTemplate == null)
+ {
+ continue;
+ }
+
+ ScanGameObject(dropdownTemplate.transform.root.gameObject, config);
+ }
+ #endif
+ }
+
+ private static void StripPlayableDirectorWithPrefabs(PlayableDirector playableDirector)
+ {
+ if(!(playableDirector.playableAsset is UnityEngine.Timeline.TimelineAsset timelineAsset))
+ return;
+
+ IEnumerable<TrackAsset> tracks = timelineAsset.GetOutputTracks();
+ foreach(TrackAsset track in tracks)
+ {
+ if(!(track is ControlTrack))
+ continue;
+
+ IEnumerable<TimelineClip> clips = track.GetClips();
+ foreach(TimelineClip clip in clips)
+ {
+ if(clip.asset is ControlPlayableAsset controlPlayableAsset && controlPlayableAsset.prefabGameObject != null)
+ {
+ UnityEngine.Object.Destroy(playableDirector);
+ VRC.Core.Logger.LogWarning("PlayableDirector containing prefab removed", DebugLevel, playableDirector.gameObject);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs.meta b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs.meta
new file mode 100644
index 00000000..f84c2b09
--- /dev/null
+++ b/VRCSDK3AvatarsLegacy/Assets/VRCSDK/Dependencies/VRChat/Scripts/Validation/WorldValidation.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 9b03724cd556cb047b2da80492ea28a5
+timeCreated: 1504829091
+licenseType: Pro
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: