using System; using JetBrains.Annotations; using UdonSharp; using UnityEngine; using UnityEngine.Serialization; using VRC.SDK3.Data; using VRC.SDK3.Image; using VRC.SDK3.StringLoading; using VRC.SDKBase; using VRC.Udon.Common.Interfaces; using VRCBoard; using VRCBoard.Components; using Object = UnityEngine.Object; using UnityEditor; namespace VRCBoard { public enum IDType { Tier, Tag, Image } public class VRCBoardManager : UdonSharpBehaviour { public bool debugMode = true; public string vrcBoardDomain = "mydomain.vrcboard.app"; public float periodicUpdateInterval = 60f; public string VrcBoardBaseUrl => "https://" + vrcBoardDomain + "/"; public int atlasUrlCount = 100; public VRCBoardBaseComponent[] vrcBoardComponents; public VRCUrl[] atlasUrls; private VRCImageDownloader[] _imageDownloaders; private Texture2D[] _downloadedAtlasTextures; private float[] _atlasDownloadTimes; public VRCUrl[] customModuleUrls; public bool[] customModuleEnabled; public VRCUrl atlasInfoUrl; private DataDictionary _atlasInfo; public VRCUrl supporterInfoUrl; private DataDictionary _supporterInfo; private DataDictionary _supporterInfoVrcLookup; private DataList _supporterList; private DataList _perkNodes; [UdonSynced, FieldChangeCallback(nameof(instanceOwnerOverride))] private string _instanceOwnerOverride = ""; private string instanceOwnerOverride { get => _instanceOwnerOverride; set { _instanceOwnerOverride = value; RequestSerialization(); Debug.Log("Instance owner override set to " + value); } } [UdonSynced, FieldChangeCallback(nameof(InstanceOwner))] private string _instanceOwner; public string[] imageIdDownloadQueue = new string[0]; [PublicAPI] public void SetInstanceOwner(string owner) { instanceOwnerOverride = owner; Networking.SetOwner(Networking.LocalPlayer, gameObject); } public string InstanceOwner { get { if (!string.IsNullOrWhiteSpace(instanceOwnerOverride)) return instanceOwnerOverride; return _instanceOwner; } private set { _instanceOwner = value; Networking.SetOwner(Networking.LocalPlayer, gameObject); RequestSerialization(); Debug.Log("Instance owner set to " + value); } } private void RemoveNullComponents() { int nullCount = 0; for (int i = 0; i < vrcBoardComponents.Length; i++) { if (vrcBoardComponents[i] == null) { nullCount++; } } if (nullCount == 0) return; VRCBoardBaseComponent[] newComponents = new VRCBoardBaseComponent[vrcBoardComponents.Length - nullCount]; int newIndex = 0; for (int i = 0; i < vrcBoardComponents.Length; i++) { if (vrcBoardComponents[i] == null) continue; newComponents[newIndex] = vrcBoardComponents[i]; newIndex++; } vrcBoardComponents = newComponents; } private void Start() { int urlCount = atlasUrls.Length; _imageDownloaders = new VRCImageDownloader[urlCount]; _downloadedAtlasTextures = new Texture2D[urlCount]; _atlasDownloadTimes = new float[urlCount]; for (int i = 0; i < atlasUrls.Length; i++) { _imageDownloaders[i] = new VRCImageDownloader(); } RemoveNullComponents(); foreach (var component in vrcBoardComponents) component._Register(this); _PeriodicUpdate(); _TaskLoop(); Debug.Log("VRCBoardManager started"); } public void _PeriodicUpdate() { //Debug.Log("Periodic update"); if (Networking.LocalPlayer.isInstanceOwner) { InstanceOwner = Networking.LocalPlayer.displayName; } _RequestAtlasInfoUpdate(); _RequestSupporterInfoUpdate(); SendCustomEventDelayedSeconds(nameof(_PeriodicUpdate), periodicUpdateInterval); } public void _TaskLoop() { //Debug.Log("Task loop"); _TryUpdateAtlasInfo(); _TryUpdateSupporterInfo(); _TryUpdateCustomModules(); SendCustomEventDelayedSeconds(nameof(_TaskLoop), 1f); } private bool _shouldUpdateAtlasInfo = true; private bool _shouldUpdateSupporterInfo = true; private bool _shouldUpdateCustomModules = false; private void _TryUpdateAtlasInfo() { if (!_shouldUpdateAtlasInfo) return; Debug.Log("Trying to update atlas info !"); RequestUrlLoad(atlasInfoUrl); _shouldUpdateAtlasInfo = false; } private void _TryUpdateSupporterInfo() { if (!_shouldUpdateSupporterInfo) return; Debug.Log("Trying to update supporter info !"); RequestUrlLoad(supporterInfoUrl); _shouldUpdateSupporterInfo = false; } private void _TryUpdateCustomModules() { if (!_shouldUpdateCustomModules) return; Debug.Log("Trying to update custom modules"); for (int i = 0; i < customModuleUrls.Length; i++) { if (!customModuleEnabled[i]) continue; VRCUrl url = customModuleUrls[i]; RequestUrlLoad(url); } _shouldUpdateCustomModules = false; } [PublicAPI] public void _RequestAtlasInfoUpdate() { _shouldUpdateAtlasInfo = true; } [PublicAPI] public void _RequestSupporterInfoUpdate() { _shouldUpdateSupporterInfo = true; } private VRCUrl[] _currentlyDownloadingUrls = new VRCUrl[0]; private void RequestUrlLoad(VRCUrl url) { string urlStr = url.Get(); for (int i = 0; i < _currentlyDownloadingUrls.Length; i++) { if (_currentlyDownloadingUrls[i].Get() == urlStr) { return; } } VRCStringDownloader.LoadUrl(url, (IUdonEventReceiver)this); VRCUrl[] newUrls = new VRCUrl[_currentlyDownloadingUrls.Length + 1]; for (int i = 0; i < _currentlyDownloadingUrls.Length; i++) { newUrls[i] = _currentlyDownloadingUrls[i]; } newUrls[_currentlyDownloadingUrls.Length] = url; _currentlyDownloadingUrls = newUrls; } public override void OnStringLoadSuccess(IVRCStringDownload download) { string url = download.Url.Get(); VRCUrl[] newUrls = new VRCUrl[_currentlyDownloadingUrls.Length - 1]; int newIndex = 0; for (int i = 0; i < _currentlyDownloadingUrls.Length; i++) { if (_currentlyDownloadingUrls[i].Get() == url) { continue; } newUrls[newIndex] = _currentlyDownloadingUrls[i]; newIndex++; } _currentlyDownloadingUrls = newUrls; //Debug.Log("String load success " + url); if (url == atlasInfoUrl.Get()) { OnAtlasInfoDownload(download.Result); } else if (url == supporterInfoUrl.Get()) { OnSupporterInfoDownload(download.Result); } for (int i = 0; i < customModuleUrls.Length; i++) { if (customModuleUrls[i].Get() == url) { OnCustomModuleDownload(i, download.Result); } } } public override void OnStringLoadError(IVRCStringDownload result) { base.OnStringLoadError(result); Debug.LogWarning(result.Url.Get() + " : " + result.Error.ToString()); } public DataDictionary _GetImageIdInfo(string imageId) { if (_atlasInfo == null) return null; if (!_atlasInfo.ContainsKey(imageId)) return null; DataToken token = _atlasInfo[imageId]; if (token.TokenType != TokenType.DataDictionary) { Debug.LogWarning("[_GetImageIdInfo] " + imageId + " does not exist."); } return token.DataDictionary; } public bool _GetImageAtlasTexture(string imageId, string uploader, out int atlasIndex, out Texture2D texture, out int position, out int size, out string type, out string hash) { texture = null; position = -1; size = -1; atlasIndex = -1; type = null; hash = null; DataDictionary imageInfo = _GetImageIdInfo(imageId); if (imageInfo == null) { Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " not found!"); return false; } bool imageTypeSuccess = imageInfo.TryGetValue("type", out DataToken typeToken); bool uploadsSuccess = imageInfo.TryGetValue("uploads", out DataToken uploadsToken); bool atlasSizeSuccess = imageInfo.TryGetValue("size", out DataToken sizeToken); if ((!imageTypeSuccess || !uploadsSuccess || !atlasSizeSuccess) || (typeToken.TokenType != TokenType.String || uploadsToken.TokenType != TokenType.DataList || sizeToken.TokenType != TokenType.Double)) { Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " is not a valid atlas."); return false; } type = typeToken.String; size = (int)sizeToken.Double; bool isGlobal = type == "global"; DataList uploads = uploadsToken.DataList; int uploadCount = uploads.Count; if (uploadCount == 0) { return false; } // find index of the uploader int uploaderIndex = 0; if (!isGlobal) { uploaderIndex = -1; if (uploader == null) return false; for (int i = 0; i < uploadCount; i++) { DataDictionary upload = uploads[i].DataDictionary; if (upload == null) continue; bool uploaderSuccess = upload.TryGetValue("a", out DataToken uploaderToken); //Debug.Log("Uploader success: " + uploaderSuccess); if (!uploaderSuccess) continue; if (uploaderToken.TokenType != TokenType.String) continue; string uploadUploader = uploaderToken.String; //Debug.Log("Comparing uploader " + uploadUploader + " to " + uploader); if (uploadUploader == uploader) { //Debug.Log("Match"); uploaderIndex = i; break; } } } //Debug.Log("Uploader index: " + uploaderIndex); if (uploaderIndex == -1) { return false; } DataDictionary uploadInfo = uploads[uploaderIndex].DataDictionary; if (uploadInfo == null) { return false; } bool atlasIndexSuccess = uploadInfo.TryGetValue("i", out DataToken atlasIndexToken); if (!atlasIndexSuccess || atlasIndexToken.TokenType != TokenType.Double) { return false; } atlasIndex = (int)atlasIndexToken.Double; bool positionSuccess = uploadInfo.TryGetValue("p", out DataToken positionToken); if (!positionSuccess || positionToken.TokenType != TokenType.Double) { return false; } texture = _downloadedAtlasTextures[atlasIndex]; position = (int)positionToken.Double; bool hashSuccess = uploadInfo.TryGetValue("h", out DataToken hashToken); if (!hashSuccess || hashToken.TokenType != TokenType.String) { return false; } hash = hashToken.String; return true; } public void _RequestImageLoad(string imageId, string uploader, VRCBoardBaseComponent component) { if (uploader == null) uploader = "null"; //Debug.Log("Requesting image load " + imageId); if (string.IsNullOrEmpty(imageId)) return; string combinedId = $"{imageId}:{uploader}"; if (HasImageInQueue(combinedId)) return; AddImageToQueue(combinedId); } private void TryDownloadImageId(string combinedImageId) { string[] split = combinedImageId.Split(':'); if (split.Length != 2) return; string imageId = split[0]; string uploader = split[1]; if (uploader == "null") uploader = null; bool success = _GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture, out int position, out int size, out string type, out string hash); //Debug.Log("Try download image " + combinedImageId + " success: " + success); if (!success) return; DownloadAtlas(atlasIndex); } private void OnSupporterInfoDownload(string data) { Debug.Log("[VRCBoard] Supporter info download success"); if (VRCJson.TryDeserializeFromJson(data, out DataToken result)) { if (result.TokenType != TokenType.DataDictionary) return; SetSupporterInfo(result.DataDictionary); OnSupporterInfoUpdate(); } else { Debug.LogError("[VRCBoard] Failed to deserialize supporter info"); } } private void SetSupporterInfo(DataDictionary value) { _supporterInfo = value; if (_supporterInfo == null) return; bool vrcsuccess = _supporterInfo.TryGetValue("vrclookup", out DataToken vrcToken); bool supporterSuccess = _supporterInfo.TryGetValue("supporters", out DataToken supporterToken); bool perkSuccess = _supporterInfo.TryGetValue("perkNodes", out DataToken perkToken); if (vrcsuccess && vrcToken.TokenType == TokenType.DataDictionary) { _supporterInfoVrcLookup = vrcToken.DataDictionary; } if (supporterSuccess && supporterToken.TokenType == TokenType.DataList) { _supporterList = supporterToken.DataList; } if (perkSuccess && perkToken.TokenType == TokenType.DataList) { _perkNodes = perkToken.DataList; } //Debug.Log("Set supporter info "+vrcsuccess+" "+supporterSuccess+" "+perkSuccess); } private void OnCustomModuleDownload(int moduleIndex, string data) { Debug.Log("[VRCBoard] Custom module download success " + moduleIndex); if (VRCJson.TryDeserializeFromJson(data, out DataToken result)) { if (result.TokenType != TokenType.DataDictionary) return; OnCustomModuleUpdate(moduleIndex, result.DataDictionary); } else { Debug.LogError("[VRCBoard] Failed to deserialize custom module " + moduleIndex); } } private void OnCustomModuleUpdate(int moduleIndex, DataDictionary data) { Debug.Log("[VRCBoard] Custom module update " + moduleIndex); foreach (var component in vrcBoardComponents) component._OnCustomModuleUpdate(moduleIndex, data); } [PublicAPI] public bool PlayerHasTag(VRCPlayerApi player, string tag) { return PlayerHasTag(player.displayName, tag); } [PublicAPI] public String[] GetPlayerTags(string displayName) { DataDictionary supporterData = GetSupporterDataFromPlayer(displayName); if (supporterData == null) return new string[0]; bool perkNodesSuccess = supporterData.TryGetValue("pn", out DataToken perkNodesToken); if (!perkNodesSuccess || perkNodesToken.TokenType != TokenType.DataList) return new string[0]; DataList perkNodes = perkNodesToken.DataList; string[] tags = new string[perkNodes.Count]; for (int i = 0; i < perkNodes.Count; i++) { DataToken perkNode = perkNodes[i]; if (perkNode.TokenType != TokenType.String) continue; tags[i] = perkNode.String; } return tags; } [PublicAPI] public bool PlayerHasTag(string displayName, string tag) { string[] tags = GetPlayerTags(displayName); for (int i = 0; i < tags.Length; i++) { if (tags[i] == tag) return true; } return false; } [PublicAPI] public int SupporterCount => _supporterList.Count; [PublicAPI] public DataDictionary GetSupporterData(int index) { DataToken token = _supporterList[index]; if (token.TokenType != TokenType.DataDictionary) return null; DataDictionary supporterData = token.DataDictionary; string perkNodesKey = "pn"; bool perkNodesSuccess = supporterData.TryGetValue(perkNodesKey, out DataToken pnToken); if (!perkNodesSuccess || pnToken.TokenType != TokenType.DataList) return supporterData; DataList perkNodes = pnToken.DataList; for (int i = 0; i < perkNodes.Count; i++) { DataToken perkNodeToken = perkNodes[i]; if (perkNodeToken.TokenType != TokenType.Double) continue; int perkIndex = (int)perkNodeToken.Double; if (perkIndex < 0 || perkIndex >= _perkNodes.Count) continue; DataToken perkNode = _perkNodes[perkIndex]; if (perkNode.TokenType != TokenType.DataDictionary) continue; bool perkNameSuccess = perkNode.DataDictionary.TryGetValue("id", out DataToken perkNameToken); if (!perkNameSuccess || perkNameToken.TokenType != TokenType.String) continue; perkNodes[i] = perkNameToken; } supporterData[perkNodesKey] = perkNodes; return supporterData; } [PublicAPI] public DataDictionary GetSupporterDataFromPlayer(string vrcName) { if (_supporterInfoVrcLookup == null) return null; if (!_supporterInfoVrcLookup.ContainsKey(vrcName)) return null; DataToken token = _supporterInfoVrcLookup[vrcName]; if (token.TokenType != TokenType.Double) return null; int index = (int)token.Double; return GetSupporterData(index); } [PublicAPI] public DataDictionary GetSupporterDataFromPlayer(VRCPlayerApi player) { return GetSupporterDataFromPlayer(player.displayName); } private void OnSupporterInfoUpdate() { Debug.Log("[VRCBoard] Supporter info updated"); foreach (var component in vrcBoardComponents) component._OnSupporterDataUpdate(); } private void OnAtlasInfoDownload(string data) { //Debug.Log("Atlas info download success"); if (VRCJson.TryDeserializeFromJson(data, out DataToken result)) { if (result.TokenType != TokenType.DataDictionary) return; _atlasInfo = result.DataDictionary; OnAtlasInfoUpdate(); } else { Debug.LogError("[VRCBoard] Failed to deserialize atlas info"); } } private void OnAtlasInfoUpdate() { //Debug.Log("Atlas info updated"); if (_atlasInfo == null) return; while (true) { string combinedImageId = GetNextImageInQueue(); if (combinedImageId == null) break; TryDownloadImageId(combinedImageId); } foreach (var component in vrcBoardComponents) component._OnImageInfoUpdate(); } private void DownloadAtlas(int index) { //Debug.Log("Downloading atlas " + index); if (index < 0 || index >= atlasUrls.Length) return; VRCUrl url = GetAtlasUrlFromIndex(index); VRCImageDownloader imageDownloader = _imageDownloaders[index]; imageDownloader.Dispose(); if (url == null) return; TextureInfo textureInfo = new TextureInfo(); textureInfo.FilterMode = FilterMode.Bilinear; textureInfo.WrapModeU = TextureWrapMode.Clamp; textureInfo.WrapModeV = TextureWrapMode.Clamp; textureInfo.WrapModeW = TextureWrapMode.Clamp; IVRCImageDownload download = imageDownloader.DownloadImage(url, null, (IUdonEventReceiver)this, textureInfo); } public override void OnImageLoadSuccess(IVRCImageDownload result) { string url = result.Url.Get(); Debug.Log("[VRCBoard] Image load success " + url); Texture2D texture = result.Result; int index = GetAtlasIndexFromUrl(url); if (index == -1) return; _downloadedAtlasTextures[index] = texture; _atlasDownloadTimes[index] = Time.time; Debug.Log("[VRCBoard] Downloaded atlas texture " + index); foreach (var component in vrcBoardComponents) component._OnAtlasImageLoaded(index); } private static int NextPowerOf2(int value) { if (value <= 0) return 1; // Use bitwise manipulation to find the next power of 2 value--; value |= value >> 1; value |= value >> 2; value |= value >> 4; value |= value >> 8; value |= value >> 16; return value + 1; } private string[] textureCacheKeys = new string[0]; private Texture2D[] textureCacheTextures = new Texture2D[0]; private int textureCacheCurrentSize = 0; public void AddToTextureCache(string key, Texture2D texture) { // If the store is full, resize the arrays if (textureCacheCurrentSize >= textureCacheKeys.Length) { ResizeArrays(textureCacheCurrentSize + 1); } // Add the new key-value pair textureCacheKeys[textureCacheCurrentSize-1] = key; textureCacheTextures[textureCacheCurrentSize-1] = texture; } // Method to get a texture by its key public Texture2D GetFromTextureCache(string key) { for (int i = 0; i < textureCacheCurrentSize; i++) { if (textureCacheKeys[i] == key) { return textureCacheTextures[i]; } } //Debug.LogWarning("[VRCBoard] Key not found: " + key); return null; } private void ResizeArrays(int newSize) { string[] newKeys = new string[newSize]; Texture2D[] newTextures = new Texture2D[newSize]; // Copy existing data to the new arrays for (int i = 0; i < textureCacheCurrentSize; i++) { newKeys[i] = textureCacheKeys[i]; newTextures[i] = textureCacheTextures[i]; } // Replace old arrays with the new resized arrays textureCacheKeys = newKeys; textureCacheTextures = newTextures; textureCacheCurrentSize = newSize; } [PublicAPI] public Texture2D GetTexture2D(string uploader, string imageId, out string imageHash) { bool success = _GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture, out int position, out int size, out string type, out string hash); imageHash = hash; Texture2D cachedTexture = GetFromTextureCache(hash); if (cachedTexture != null) return cachedTexture; if (!success || atlasIndex == -1) { Debug.LogWarning("[VRCBoard] [GetTexture2D] Failed to get texture for " + uploader); return null; } if (texture == null) { Debug.LogWarning("[VRCBoard] [GetTexture2D] Failed to get texture for " + uploader); return null; } int blockSize = (int)Mathf.Floor((float)(texture.width) / (float)(size)); float startX = position % size; float startY = size - (int)Mathf.Floor((float)position / (float)size) - 1; startX = startX / size; startY = startY / size; //Debug.Log("[VRCBoard] texture startX:" + startX + ", startY:" + startY); int textureSize = 1024; Texture2D newTexture = new Texture2D (textureSize, textureSize); Color[] stretchedPixels = new Color[textureSize * textureSize]; for (int y = 0; y < textureSize; y++) { for (int x = 0; x < textureSize; x++) { // Get the corresponding pixel from the original texture float u = startX + ((float)x / textureSize / size); float v = startY + ((float)y / textureSize / size); // Sample the original texture at the calculated UV coordinates Color pixel = texture.GetPixelBilinear(u, v); stretchedPixels[y * textureSize + x] = pixel; } } //Color[] pixels = texture.GetPixels(startX * blockSize, startY * blockSize, blockSize, blockSize); newTexture.SetPixels(stretchedPixels); newTexture.wrapMode = TextureWrapMode.Clamp; newTexture.filterMode = FilterMode.Trilinear; newTexture.anisoLevel = 1; newTexture.Apply(); AddToTextureCache(hash, newTexture); return newTexture; } [PublicAPI] public Texture2D GetTexture2D(string uploader, string imageId) { return GetTexture2D(uploader, imageId, out string imageHash); } public override void OnImageLoadError(IVRCImageDownload result) { base.OnImageLoadError(result); Debug.LogWarning(result.Url.Get() + " : " + result.Error.ToString()); } private int GetAtlasIndexFromUrl(string url) { for (int i = 0; i < atlasUrls.Length; i++) { if (atlasUrls[i].Get() == url) return i; } return -1; } private VRCUrl GetAtlasUrlFromIndex(int index) { if (index < 0 || index >= atlasUrls.Length || atlasUrls[index] == null) { return null; } return atlasUrls[index]; } #region Queue Functions private void AddImageToQueue(string imageId) { string[] newQueue = new string[imageIdDownloadQueue.Length + 1]; for (int i = 0; i < imageIdDownloadQueue.Length; i++) { newQueue[i] = imageIdDownloadQueue[i]; } newQueue[imageIdDownloadQueue.Length] = imageId; imageIdDownloadQueue = newQueue; _RequestAtlasInfoUpdate(); } private bool HasImageInQueue(string imageId) { for (int i = 0; i < imageIdDownloadQueue.Length; i++) { if (imageIdDownloadQueue[i] == imageId) { return true; } } return false; } private string GetNextImageInQueue() { if (imageIdDownloadQueue.Length == 0) return null; string imageId = imageIdDownloadQueue[0]; string[] newQueue = new string[imageIdDownloadQueue.Length - 1]; for (int i = 1; i < imageIdDownloadQueue.Length; i++) { newQueue[i - 1] = imageIdDownloadQueue[i]; } imageIdDownloadQueue = newQueue; return imageId; } #endregion } } #if UNITY_EDITOR && !UDONSHARP_COMPILER namespace VRCBoard { [CustomEditor(typeof(VRCBoardManager))] public class VrcBoardManagerEditor : Editor { bool shouldRefreshUrls = true; private void OnEnable() { shouldRefreshUrls = true; } public override void OnInspectorGUI() { // is first render //base.OnInspectorGUI(); VRCBoardManager manager = target as VRCBoardManager; if (manager == null) { EditorGUILayout.HelpBox("This script is not attached to a VRCBoardManager object", MessageType.Error); return; } EditorGUILayout.HelpBox("VRCBoard Manager", MessageType.Info); string domainInput = EditorGUILayout.TextField("Your VRCBoard Domain", manager.vrcBoardDomain); bool changed = domainInput != manager.vrcBoardDomain; int urlCount = EditorGUILayout.IntField("Atlas Url Count [10-1000]", manager.atlasUrlCount); bool urlCountChanged = urlCount != manager.atlasUrlCount; manager.atlasUrlCount = Math.Clamp(urlCount, 10, 1000); if (changed || urlCountChanged || shouldRefreshUrls) { domainInput = domainInput.Trim(); domainInput = domainInput.Replace("http://", ""); domainInput = domainInput.Replace("https://", ""); domainInput = domainInput.Replace("/", ""); manager.vrcBoardDomain = domainInput; string baseUrl = manager.VrcBoardBaseUrl; // update the urls manager.atlasUrls = new VRCUrl[manager.atlasUrlCount]; for (int i = 0; i < manager.atlasUrlCount; i++) { manager.atlasUrls[i] = new VRCUrl(baseUrl + "atlas/" + i); } const string apiPath = "api/data/v1/"; bool[] oldEnabled = manager.customModuleEnabled; manager.customModuleUrls = new VRCUrl[3]; manager.customModuleEnabled = new bool[3]; manager.customModuleUrls[0] = new VRCUrl(baseUrl + apiPath + "barmanager"); for (int i = 0; i < oldEnabled.Length; i++) { if (i < manager.customModuleEnabled.Length) manager.customModuleEnabled[i] = oldEnabled[i]; } manager.atlasInfoUrl = new VRCUrl(baseUrl + apiPath + "atlas"); manager.supporterInfoUrl = new VRCUrl(baseUrl + apiPath + "supporters"); EditorUtility.SetDirty(manager); } float selectedInterval = EditorGUILayout.FloatField("Periodic Update Interval", manager.periodicUpdateInterval); manager.periodicUpdateInterval = Mathf.Clamp(selectedInterval, 5f, 600f); EditorGUILayout.Separator(); SerializedProperty customModuleEnabled = serializedObject.FindProperty("customModuleEnabled"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("[Module] Group Manager"); // dropdown with enabled/disabled customModuleEnabled.GetArrayElementAtIndex(0).boolValue = EditorGUILayout.Popup(customModuleEnabled.GetArrayElementAtIndex(0).boolValue ? 1 : 0, new[] {"Disabled", "Enabled"}) == 1; EditorGUILayout.EndHorizontal(); EditorGUILayout.Separator(); SerializedProperty vrcBoardComponents = serializedObject.FindProperty("vrcBoardComponents"); EditorGUILayout.PropertyField(vrcBoardComponents); if (GUILayout.Button("Link all VRCBoard components")) { VRCBoardBaseComponent[] components = FindObjectsOfType(true); manager.vrcBoardComponents = components; // mark the object as dirty EditorUtility.SetDirty(manager); } serializedObject.ApplyModifiedProperties(); //DrawDefaultInspector(); shouldRefreshUrls = false; } } } #endif