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 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 = ""; public string instanceOwnerOverride { get => _instanceOwnerOverride; set { _instanceOwnerOverride = value; Networking.SetOwner(Networking.LocalPlayer, gameObject); RequestSerialization(); Debug.Log("Instance owner override set to " + value); } } [UdonSynced, FieldChangeCallback(nameof(instanceOwner))] private string _instanceOwner; public string[] imageIdDownloadQueue = new string[0]; public string instanceOwner { get { if (!string.IsNullOrWhiteSpace(instanceOwnerOverride)) return instanceOwnerOverride; return _instanceOwner; } 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(); SendCustomEventDelayedSeconds(nameof(_TaskLoop), 1f); } private bool _shouldUpdateAtlasInfo = true; private bool _shouldUpdateSupporterInfo = true; 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; } [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); } } 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) return null; 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) { texture = null; position = -1; size = -1; atlasIndex = -1; type = null; DataDictionary imageInfo = _GetImageIdInfo(imageId); if (imageInfo == null) { 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); //Debug.Log("Get image atlas texture " + imageId + " success: " + imageTypeSuccess + " " + uploadsSuccess + " " + atlasSizeSuccess); if ((!imageTypeSuccess || !uploadsSuccess || !atlasSizeSuccess) || (typeToken.TokenType != TokenType.String || uploadsToken.TokenType != TokenType.DataList || sizeToken.TokenType != TokenType.Double)) { 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; return true; } public void _RequestImageLoad(string imageId, string uploader, VRCBoardBaseComponent component) { //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]; bool success = _GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture, out int position, out int size, out string type); //Debug.Log("Try download image " + combinedImageId + " success: " + success); if (!success) return; DownloadAtlas(atlasIndex); } private void OnSupporterInfoDownload(string data) { Debug.Log("Supporter info download success"); if (VRCJson.TryDeserializeFromJson(data, out DataToken result)) { if (result.TokenType != TokenType.DataDictionary) return; SetSupporterInfo(result.DataDictionary); OnSupporterInfoUpdate(); } else { Debug.LogError("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); } [PublicAPI] public bool PlayerHasTag(VRCPlayerApi player, string tag) { return PlayerHasTag(player.displayName, tag); } [PublicAPI] public bool PlayerHasTag(string displayName, string tag) { DataDictionary supporterData = GetSupporterDataFromPlayer(displayName); if (supporterData == null) return false; bool perkNodesSuccess = supporterData.TryGetValue("pn", out DataToken perkNodesToken); if (!perkNodesSuccess || perkNodesToken.TokenType != TokenType.DataList) return false; DataList perkNodes = perkNodesToken.DataList; for (int i = 0; i < perkNodes.Count; i++) { DataToken perkNode = perkNodes[i]; if (perkNode.TokenType != TokenType.String) continue; if (perkNode.String == 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("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("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("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("Downloaded atlas texture " + index); foreach (var component in vrcBoardComponents) component._OnAtlasImageLoaded(index); } [PublicAPI] public Texture2D GetTexture2D(string uploader, string imageId) { bool success = _GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture, out int position, out int size, out string type); 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)); int startX = position % size; int startY = size - (int)Mathf.Floor((float)position / (float)size) - 1; // Debug.Log("texture startX:" + startX + ", startY:" + startY); int blockWidth = blockSize; int blockHeight = blockSize; Texture2D newTexture = new Texture2D (blockWidth, blockHeight); Color[] pixels = texture.GetPixels(startX * blockWidth, startY * blockHeight, blockWidth, blockHeight); newTexture.SetPixels(pixels); newTexture.wrapMode = TextureWrapMode.Clamp; newTexture.filterMode = FilterMode.Trilinear; newTexture.anisoLevel = 1; //newTexture.alphaIsTransparency = true; newTexture.Apply(); return newTexture; } 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 { public override void OnInspectorGUI() { //base.OnInspectorGUI(); VRCBoardManager manager = target as VRCBoardManager; if (manager == null) { EditorGUILayout.HelpBox("This script is not attached to a VRCBoardManager object", MessageType.Error); return; } // draw a header 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) { 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/"; 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.Space(); SerializedProperty vrcBoardComponents = serializedObject.FindProperty("vrcBoardComponents"); EditorGUILayout.PropertyField(vrcBoardComponents); if (GUILayout.Button("Link all VRCBoard components")) { VRCBoardBaseComponent[] components = FindObjectsOfType(); manager.vrcBoardComponents = components; // mark the object as dirty EditorUtility.SetDirty(manager); } serializedObject.ApplyModifiedProperties(); //DrawDefaultInspector(); } } } #endif