Files
VRCBoard-Udon/VRCBoardManager.cs
Aaro Varis cf5f71e34f
All checks were successful
Create Unity Package / package (push) Successful in 8s
feat: GitHub integration and URL management
2025-11-25 18:38:00 +02:00

1044 lines
40 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class VRCBoardManager : UdonSharpBehaviour
{
public string GIT_USERNAME = "";
public string GIT_REPO = "";
public string GIT_BASE_URL => "https://raw.githubusercontent.com/" + GIT_USERNAME + "/" + GIT_REPO + "/refs/heads/main/";
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 byte waitIndex = 0;
private bool _useProxyUrls = false;
#region URLS
public VRCUrl[] _sourceAtlasUrls;
public VRCUrl[] _proxyAtlasUrls;
public VRCUrl _sourceAtlasInfoUrl;
public VRCUrl _proxyAtlasInfoUrl;
public VRCUrl[] _sourceCustomModuleUrls;
public VRCUrl[] _proxyCustomModuleUrls;
public VRCUrl _sourceSupporterInfoUrl;
public VRCUrl _proxySupporterInfoUrl;
public VRCUrl[] customModuleUrls => _useProxyUrls ? _proxyCustomModuleUrls : _sourceCustomModuleUrls;
public VRCUrl supporterInfoUrl => _useProxyUrls ? _proxySupporterInfoUrl : _sourceSupporterInfoUrl;
public VRCUrl atlasInfoUrl => _useProxyUrls ? _proxyAtlasInfoUrl : _sourceAtlasInfoUrl;
public VRCUrl[] atlasUrls => _useProxyUrls ? _proxyAtlasUrls : _sourceAtlasUrls;
#endregion
private VRCImageDownloader[] _imageDownloaders;
private Texture2D[] _downloadedAtlasTextures;
private float[] _atlasDownloadTimes;
public bool[] customModuleEnabled;
private DataDictionary _atlasInfo;
private DataDictionary _supporterInfo;
private DataDictionary _supporterInfoVrcLookup;
private DataList _supporterList;
private DataList _perkNodes;
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];
public bool automaticInstanceOwner = true;
[PublicAPI]
public void SetInstanceOwner(string owner)
{
instanceOwnerOverride = owner;
}
public string InstanceOwner
{
get {
if (!string.IsNullOrWhiteSpace(instanceOwnerOverride)) return instanceOwnerOverride;
if (automaticInstanceOwner)
{
return _instanceOwner;
}
return String.Empty;
}
private set
{
_instanceOwner = value;
}
}
public string _VRCBoardIdFromName(string name)
{
DataDictionary ownerInfo = GetSupporterDataFromPlayer(name);
if (ownerInfo == null)
{
Debug.LogWarning("Failed to get user info, user has likely not linked their account. username: " + name);
return "";
}
bool ownerSuccess = ownerInfo.TryGetValue("id", out DataToken ownerToken);
if (!ownerSuccess || ownerToken.TokenType != TokenType.String)
{
Debug.LogWarning("Failed to get vrcboard ID from displayname");
return "";
}
return ownerToken.String;
}
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)
{
Debug.Log("[VRCBOARD] Laoding URL " + url.Get());
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)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " has no uploads.");
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)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " uploader not found. uploaderIndex==-1");
return false;
}
DataDictionary uploadInfo = uploads[uploaderIndex].DataDictionary;
if (uploadInfo == null)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " uploadInfo is null.");
return false;
}
bool atlasIndexSuccess = uploadInfo.TryGetValue("i", out DataToken atlasIndexToken);
if (!atlasIndexSuccess || atlasIndexToken.TokenType != TokenType.Double)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " atlasIndex is not a valid number.");
return false;
}
atlasIndex = (int)atlasIndexToken.Double;
bool positionSuccess = uploadInfo.TryGetValue("p", out DataToken positionToken);
if (!positionSuccess || positionToken.TokenType != TokenType.Double)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " position is not a valid number.");
return false;
}
texture = _downloadedAtlasTextures[atlasIndex];
position = (int)positionToken.Double;
bool hashSuccess = uploadInfo.TryGetValue("h", out DataToken hashToken);
if (!hashSuccess || hashToken.TokenType != TokenType.String)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " hash is not a valid string.");
return false;
}
hash = hashToken.String;
return true;
}
public void _RequestCustomModuleUpdate(int moduleIndex)
{
if (moduleIndex < 0 || moduleIndex >= customModuleUrls.Length) return;
VRCUrl url = customModuleUrls[moduleIndex];
RequestUrlLoad(url);
}
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)
{
Debug.Log("[VRCBoard] Trying to download imageid " + 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;
/*if ( GetFromTextureCache(hash) != null)
{
Debug.Log("[VRCBoard] Image found in texture cache " + combinedImageId);
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("[VRCBoard] 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;
}
public void BilinearScale(Texture2D tex, int newWidth, int newHeight)
{
Color[] newPixels = new Color[newWidth * newHeight];
Color[] originalPixels = tex.GetPixels();
int origWidth = tex.width;
int origHeight = tex.height;
float ratioX = (float)(origWidth - 1) / newWidth;
float ratioY = (float)(origHeight - 1) / newHeight;
for (int y = 0; y < newHeight; y++)
{
float sy = y * ratioY;
int yFloor = Mathf.FloorToInt(sy);
int yCeil = Mathf.Min(yFloor + 1, origHeight - 1);
float fy = sy - yFloor;
for (int x = 0; x < newWidth; x++)
{
float sx = x * ratioX;
int xFloor = Mathf.FloorToInt(sx);
int xCeil = Mathf.Min(xFloor + 1, origWidth - 1);
float fx = sx - xFloor;
// Manual bilinear interpolation
Color c00 = originalPixels[yFloor * origWidth + xFloor];
Color c10 = originalPixels[yFloor * origWidth + xCeil];
Color c01 = originalPixels[yCeil * origWidth + xFloor];
Color c11 = originalPixels[yCeil * origWidth + xCeil];
Color interpolated =
c00 * (1 - fx) * (1 - fy) +
c10 * fx * (1 - fy) +
c01 * (1 - fx) * fy +
c11 * fx * fy;
newPixels[y * newWidth + x] = interpolated;
}
}
tex.Reinitialize(newWidth, newHeight);
tex.SetPixels(newPixels);
tex.Apply();
}
[PublicAPI]
public Texture2D GetTexture2D(string uploader, string imageId, out string imageHash, bool bilinearScaling = false)
{
imageHash = null;
if (!_GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture,
out int position, out int size, out string type, out string hash) || atlasIndex == -1 || texture == null)
{
Debug.LogWarning("[VRCBoard] [GetTexture2D] Failed to get texture for " + uploader);
return null;
}
imageHash = hash;
Texture2D cachedTexture = GetFromTextureCache(hash);
if (cachedTexture != null) return cachedTexture;
int blockSize = texture.width / size;
int xIndex = position % size;
int yIndex = size - 1 - (position / size);
int startX = xIndex * blockSize;
int startY = yIndex * blockSize;
Color[] pixels = texture.GetPixels(startX, startY, blockSize, blockSize);
// Instead of bilinear sampling, scale using Unitys built-in Resize method.
Texture2D newTexture = new Texture2D(blockSize, blockSize, TextureFormat.RGBA32, false);
newTexture.SetPixels(pixels);
if (bilinearScaling) BilinearScale(newTexture, 512, 512);
newTexture.wrapMode = TextureWrapMode.Repeat;
newTexture.filterMode = FilterMode.Trilinear;
newTexture.anisoLevel = 1;
newTexture.Apply(true,true);
AddToTextureCache(hash, newTexture);
return newTexture;
}
[PublicAPI]
public Texture2D GetTexture2D(string uploader, string imageId, bool bilinearScaling = false)
{
return GetTexture2D(uploader, imageId, out string imageHash, bilinearScaling);
}
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;
bool showGitSettings = false;
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._sourceAtlasUrls = new VRCUrl[manager.atlasUrlCount];
manager._proxyAtlasUrls = new VRCUrl[manager.atlasUrlCount];
for (int i = 0; i < manager.atlasUrlCount; i++)
{
manager._sourceAtlasUrls[i] = new VRCUrl(baseUrl + "atlas/" + i);
manager._proxyAtlasUrls[i] = new VRCUrl(manager.GIT_BASE_URL + "atlas/image" + i + ".png");
}
const string apiPath = "api/data/v1/";
bool[] oldEnabled = manager.customModuleEnabled;
manager._sourceCustomModuleUrls = new VRCUrl[3];
manager._proxyCustomModuleUrls = new VRCUrl[3];
manager.customModuleEnabled = new bool[3];
manager._sourceCustomModuleUrls[0] = new VRCUrl(baseUrl + apiPath + "module/barmanager");
manager._proxyCustomModuleUrls[0] = new VRCUrl(manager.GIT_BASE_URL + "custom_modules/barmanager");
for (int i = 0; i < oldEnabled.Length; i++)
{
if (i < manager.customModuleEnabled.Length) manager.customModuleEnabled[i] = oldEnabled[i];
}
manager._sourceAtlasInfoUrl = new VRCUrl(baseUrl + apiPath + "atlas");
manager._sourceSupporterInfoUrl = new VRCUrl(baseUrl + apiPath + "supporters");
manager._proxyAtlasInfoUrl = new VRCUrl(manager.GIT_BASE_URL + "atlas.json");
manager._proxySupporterInfoUrl = new VRCUrl(manager.GIT_BASE_URL + "supporters.json");
EditorUtility.SetDirty(manager);
}
float selectedInterval = EditorGUILayout.FloatField("Periodic Update Interval", manager.periodicUpdateInterval);
if (selectedInterval != manager.periodicUpdateInterval)
{
manager.periodicUpdateInterval = Mathf.Clamp(selectedInterval, 5f, 600f);
EditorUtility.SetDirty(manager);
}
bool instanceOwner = EditorGUILayout.Toggle("Enable Automatic Instance Owner Detection", manager.automaticInstanceOwner);
if (instanceOwner != manager.automaticInstanceOwner)
{
manager.automaticInstanceOwner = instanceOwner;
EditorUtility.SetDirty(manager);
}
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();
showGitSettings = EditorGUILayout.Foldout(showGitSettings, "GitHub Settings");
if (showGitSettings)
{
EditorGUILayout.HelpBox("GitHub can be used to host the images on trusted URLs. See more info on the VRCBoard website.", MessageType.Info);
string gitUser = EditorGUILayout.TextField("GitHub Username", manager.GIT_USERNAME);
string gitRepo = EditorGUILayout.TextField("GitHub Repo", manager.GIT_REPO);
//string gitBranch = EditorGUILayout.TextField("GitHub Branch", manager.GIT_BRANCH);
bool gitChanged = gitUser != manager.GIT_USERNAME || gitRepo != manager.GIT_REPO; //|| gitBranch != manager.GIT_BRANCH;
if (gitChanged)
{
manager.GIT_USERNAME = gitUser;
manager.GIT_REPO = gitRepo;
EditorUtility.SetDirty(manager);
}
}
EditorGUILayout.Separator();
SerializedProperty vrcBoardComponents = serializedObject.FindProperty("vrcBoardComponents");
EditorGUILayout.PropertyField(vrcBoardComponents);
if (GUILayout.Button("Link all VRCBoard components"))
{
VRCBoardBaseComponent[] components = FindObjectsOfType<VRCBoardBaseComponent>(true);
manager.vrcBoardComponents = components;
// mark the object as dirty
EditorUtility.SetDirty(manager);
}
serializedObject.ApplyModifiedProperties();
//DrawDefaultInspector();
shouldRefreshUrls = false;
}
}
}
#endif